Security Headers
The Security_headers class provides a structured way to apply HTTP security headers
to both HTML pages and API responses in LavaLust. It supports Content Security Policy (CSP)
with nonce generation, HSTS, CORS, referrer policy, permissions policy, and more.
It is configured via app/config/config.php under the security_headers key, and can
also accept inline overrides when instantiated.
Folder Structure
app/
└── config/
└── config.php ← security_headers key goes here
Configuration
All settings live under the security_headers key in app/config/config.php.
You only need to define the values you want to override — the rest use sensible defaults.
<?php
$config['security_headers'] = [
// ── Content Security Policy ──────────────────────────────────────────
'csp_enabled' => true,
'csp_report_only' => false, // set true to test without blocking
'report_uri' => '', // e.g. 'https://yourdomain.com/csp-report'
// Additional allowed sources per directive (merged with 'self' + nonce)
'script_src' => ['https://cdn.jsdelivr.net'],
'style_src' => ['https://fonts.googleapis.com'],
'font_src' => ['https://fonts.gstatic.com'],
'img_src' => ['https://res.cloudinary.com'],
'connect_src' => [],
'frame_src' => [],
// Allow inline style/script attributes (style="..." onclick="...")
'allow_inline_style_attr' => true,
'allow_inline_script_attr' => false,
// ── Transport ────────────────────────────────────────────────────────
'hsts_max_age' => 31536000, // 1 year
'hsts_subdomains' => true,
'trust_proxy' => false, // set true behind a reverse proxy
// ── Other headers ────────────────────────────────────────────────────
'frame_options' => 'DENY',
'referrer_policy' => 'strict-origin-when-cross-origin',
'permissions_policy' => 'geolocation=(), microphone=(), camera=(), payment=()',
'coop' => 'same-origin', // set '' to allow OAuth popups
'frame_ancestors' => "'none'", // or "'self'"
// ── API body size limit ───────────────────────────────────────────────
'api_max_body' => 1048576, // 1 MB in bytes
// ── CORS ─────────────────────────────────────────────────────────────
'cors' => [
'allowed_origins' => ['https://app.example.com', 'https://admin.example.com'],
'methods' => 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
'headers' => 'Authorization, Content-Type, X-Requested-With',
'expose_headers' => 'X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After',
'credentials' => false,
'max_age' => 600,
],
// ── Custom headers ────────────────────────────────────────────────────
'extra_headers' => [
'X-Custom-Header' => 'value',
],
];
Note
If cors.allowed_origins is left empty, the class falls back to the
allow_origin value from app/config/api.php for backwards compatibility
with the Api library.
Default Values Reference
Key |
Default |
Description |
|---|---|---|
|
|
Enable the |
|
|
Use |
|
|
URI where CSP violation reports are sent |
|
|
Trust |
|
|
Extra |
|
|
Extra |
|
|
Extra |
|
|
Extra |
|
|
Extra |
|
|
Extra |
|
|
Allow |
|
|
Allow inline event handlers via |
|
|
HSTS max-age in seconds (1 year) |
|
|
Include |
|
|
Value for |
|
|
Value for |
|
|
Value for |
|
|
Value for |
|
|
CSP |
|
|
Maximum allowed request body size in bytes (1 MB) |
|
|
Exact allowed origins. Falls back to |
|
|
Allowed HTTP methods for CORS preflight |
|
|
Allowed request headers for CORS preflight |
|
|
Headers exposed to the browser via |
|
|
Set |
|
|
Preflight cache duration in seconds |
|
|
Any additional custom headers as |
Methods
apply()
Applies security headers suitable for HTML page responses. When CSP is enabled,
a unique per-request nonce is injected into script-src and style-src automatically.
Also sets X-Frame-Options, HSTS (on HTTPS), and all common headers.
<?php
class PageController extends Controller
{
public function index()
{
$this->security_headers->apply();
$this->call->view('home/index');
}
}
apply_api()
Applies a strict set of headers for API / JSON responses. Sets a restrictive
Content-Security-Policy, disables caching, applies DENY for framing, and
calls cors() automatically.
<?php
class ApiController extends Controller
{
public function __construct()
{
parent::__construct();
$this->security_headers->apply_api();
}
public function users()
{
$this->call->model('UserModel');
$users = $this->UserModel->all();
$this->response->set_status_code(200);
$this->response->send_json($users);
}
}
api_guard($methods, $max_body)
Enforces request method, body size, and content type on API endpoints.
Responds automatically with 405, 413, or 415 on failure.
Parameter |
Type |
Description |
|---|---|---|
|
array|null |
Allowed HTTP methods. |
|
int|null |
Max body size in bytes. Falls back to |
<?php
// Allow only POST; reject oversized bodies; validate Content-Type
$this->security_headers->api_guard(['POST']);
// Allow GET and POST with a custom 512 KB body limit
$this->security_headers->api_guard(['GET', 'POST'], 524288);
// Skip method check — useful when require_method() handles it separately
$this->security_headers->api_guard(null);
cors()
Sends CORS headers based on the cors config block. Called automatically
by apply_api(). Can be called manually for pages that need CORS support.
Matches the incoming
Originheader againstcors.allowed_origins.Handles
OPTIONSpreflight requests and responds with204.Wildcard
*inallowed_originsis supported but cannot be combined withcredentials: true.
<?php
// Manual use (e.g. on a specific page route)
$this->security_headers->cors();
nonce()
Returns the CSP nonce for the current request (static, generated once per request).
Use it to mark inline <script> and <style> tags as trusted.
<?php
// In a view file
$nonce = csp_nonce(); // global helper — equivalent to Security_headers::nonce()
?>
<script nonce="<?= $nonce ?>">
console.log('Trusted inline script');
</script>
<style nonce="<?= $nonce ?>">
body { margin: 0; }
</style>
Note
The global helper function csp_nonce() is registered automatically when the
class is loaded. It is also available as the csp_nonce template variable in
LavaLust’s template engine.
max_body()
Returns the configured maximum request body size in bytes.
<?php
$limit = $this->security_headers->max_body(); // e.g. 1048576
Common Usage Patterns
HTML pages with CSP enabled
<?php
// app/config/config.php
$config['security_headers'] = [
'csp_enabled' => true,
'script_src' => ['https://cdn.jsdelivr.net'],
'style_src' => ['https://fonts.googleapis.com'],
'font_src' => ['https://fonts.gstatic.com'],
];
<?php
// In your controller
class HomeController extends Controller
{
public function index()
{
$this->security_headers->apply();
$this->call->view('home/index');
}
}
<!-- In your view: use the nonce on inline tags -->
<script nonce="<?= csp_nonce() ?>">
// trusted inline script
</script>
API controller with full protection
<?php
class ApiController extends Controller
{
public function __construct()
{
parent::__construct();
// Apply API headers on every request
$this->security_headers->apply_api();
}
public function create()
{
// Enforce POST, validate Content-Type, limit body to 1 MB
$this->security_headers->api_guard(['POST']);
$input = $this->api->body();
// ... handle the request
}
public function list()
{
// GET only, custom 256 KB limit
$this->security_headers->api_guard(['GET'], 262144);
$this->call->model('UserModel');
$this->response->send_json($this->UserModel->all());
}
}
Report-only mode (CSP testing)
Use csp_report_only to observe violations without blocking anything:
<?php
$config['security_headers'] = [
'csp_enabled' => true,
'csp_report_only' => true,
'report_uri' => 'https://yourdomain.com/csp-report',
];
OAuth / popup-compatible COOP
Setting coop to an empty string skips the Cross-Origin-Opener-Policy header,
which is required for OAuth flows that open a popup:
<?php
$config['security_headers'] = [
'coop' => '', // skip COOP — required for OAuth popups
];
Inline override at runtime
You can pass overrides directly when constructing the class:
<?php
$sh = new Security_headers([
'csp_enabled' => true,
'frame_options'=> 'SAMEORIGIN',
'cors' => [
'allowed_origins' => ['https://partner.example.com'],
],
]);
$sh->apply();
Headers Applied
apply() — HTML Pages
Header |
Notes |
|---|---|
|
Only when |
|
Default: |
|
Always |
|
Default: |
|
Default disables geolocation, microphone, camera, payment |
|
Default: |
|
Always |
|
Added on HTTPS only |
|
Removed |
apply_api() — API Responses
Header |
Notes |
|---|---|
|
Always |
|
Always |
|
Always |
|
Always |
|
Always |
All common headers above |
Same as |
CORS headers |
Applied via |
Method Reference
Method |
Description |
|---|---|
|
Applies security headers for HTML page responses |
|
Applies strict headers for API/JSON responses; calls |
|
Validates method, body size, and content type; auto-responds on failure |
|
Sends CORS headers; handles OPTIONS preflight and exits with 204 |
|
Returns the per-request CSP nonce (generated once, reused) |
|
Returns the configured max request body size in bytes |
|
Alias for |
Tips and Best Practices
Call
apply()as early as possible in a controller — ideally in__construct()for pages that always need headers, or at the top of the method for conditional pages.Call
apply_api()in the API controller’s__construct()so all endpoints automatically receive the correct headers without repeating the call.Use
api_guard()at the start of each API method to validate the request before any model or business logic runs.Enable
csp_report_onlyfirst when rolling out CSP on an existing application — collect violations atreport_uriand fix them before switching to enforcement mode.Never set
cors.credentials = truetogether withallowed_origins = ['*']— browsers will reject the response.Set
coop = ''only for routes that genuinely need OAuth popup support; leave it atsame-origineverywhere else.Use
csp_nonce()on every inline<script>and<style>tag in your views instead of adding'unsafe-inline'to the CSP directives.Add any third-party CDN URLs to the relevant
*_srcarrays in config rather than falling back to'unsafe-inline'or'unsafe-eval'.