Skip to content

HTTPS and headers

Serve the application only over HTTPS. CodeIgniter’s forcehttps filter redirects plain requests, but sends Strict-Transport-Security only on that redirect, which browsers ignore over HTTP. Set HSTS at the web server or the TLS terminating proxy:

# nginx, in the server block for 443
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Apache, mod_headers, in the VirtualHost for 443
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

Only add includeSubDomains if every subdomain is served over HTTPS.

Header Set by
X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: same-origin Anvil on panel pages, endpoint answers and API answers (Anvil\Security\SecurityHeaders, headers already set are kept)
Cache-Control: no-store Anvil on panel pages, endpoint and API answers, file and export downloads
Content-Security-Policy: sandbox Anvil on file and export downloads
X-Frame-Options: SAMEORIGIN, X-Content-Type-Options, X-Download-Options, X-Permitted-Cross-Domain-Policies, Referrer-Policy CodeIgniter’s secureheaders filter (global after) on every answer
Content-Security-Policy CodeIgniter with app.CSPEnabled = true
Strict-Transport-Security the web server or proxy

The secureheaders filter runs after Anvil and overwrites its X-Frame-Options: DENY with SAMEORIGIN. Framing is still refused: frame-ancestors 'none' in the CSP takes precedence over X-Frame-Options in current browsers. Keep the CSP enabled for this reason.

Anvil works with a strict policy, without unsafe-inline and unsafe-eval (see Styling). The AppStarter’s app/Config/ContentSecurityPolicy.php:

public bool $reportOnly = false;
public bool $upgradeInsecureRequests = false;
public $defaultSrc = 'self';
public $scriptSrc = 'self';
public array|string $scriptSrcElem = 'self';
public array|string $scriptSrcAttr = 'none';
public $styleSrc = 'self';
public array|string $styleSrcElem = 'self';
public array|string $styleSrcAttr = 'self';
public $imageSrc = ['self', 'data:'];
public $baseURI = 'self';
public $childSrc = 'self';
public $connectSrc = 'self';
public $fontSrc = 'self';
public $formAction = 'self';
public $frameAncestors = 'none';
public $objectSrc = 'none';
public bool $autoNonce = true;

autoNonce adds the nonces for Anvil’s <style> element and scripts. Pages of the application with inline scripts or style="…" attributes break under this policy; fix them instead of loosening it. To roll out a change safely, set $reportOnly = true with a $reportURI first.

The global csrf filter protects every form and htmx request (Config\Security::$csrfProtection = 'session', $regenerate = false). Exclude only what authenticates differently:

app/Config/Filters.php
'csrf' => ['except' => ['api/*']], // API: tokens, no cookies

With a SAML provider, also exclude sso/*/callback. See Installation.

Behind a load balancer or TLS terminating proxy, CodeIgniter sees the proxy’s address and plain HTTP unless the proxy is trusted. Then all users share one IP in the rate limits, the audit log records the proxy, and the API refuses every request with 403 (https required). Trust exactly your proxies:

app/Config/App.php
public array $proxyIPs = [
'10.0.1.10' => 'X-Forwarded-For',
'192.168.5.0/24' => 'X-Forwarded-For',
];

CodeIgniter then takes the client IP from that header and the protocol from X-Forwarded-Proto, only for requests from these addresses. Make sure the proxy overwrites (not appends to) these headers from clients.