Skip to content

v1.9.0

Latest

Choose a tag to compare

@sylvesterdamgaard sylvesterdamgaard released this 21 Sep 21:47

A security default that should never have been ours to get wrong, an OPcache buffer sized by measuring Laravel instead of copying Symfony, and the missing half of the FFI story.

Security

Session cookies were not HttpOnly. These images ship their own php.ini, so the engine defaults applied — session.cookie_httponly off and session.use_strict_mode off — where php.ini-production turns both on. Any XSS could read the session cookie, and a session id the attacker chose was accepted.

Laravel sets all three itself from config/session.php, so Laravel apps were never exposed. The ones that were are anything calling session_start() directly: plain PHP, WordPress, legacy code.

Now cookie_httponly=On, use_strict_mode=1, cookie_samesite=Lax. cookie_secure stays unset on purpose — it would break every local http:// container; PHP_SESSION_COOKIE_SECURE=1 is the production switch.

If your JavaScript reads the PHP session cookie directly, this is a breaking change. Set session.cookie_httponly=Off in your own conf.d drop-in.

OPcache, measured against Laravel

opcache.interned_strings_buffer was 16M. A single warm request through a real Laravel HTTP kernel interns 19.58M — so the buffer overflowed on ordinary traffic, and everything past the ceiling fell back to per-process storage, paid for by every worker, with no error anywhere. Now 32M.

The rig: Laravel 13 with livewire, horizon, spatie/laravel-permission and sanctum, config/route/view cached, booted through $app->handleRequest().

warm, one real request every .php compiled
scripts 633 10,020
memory 116.1 MB 268.2 MB
interned 19.58 MB 40.93 MB

The other two numbers were checked and deliberately left alone. memory_consumption stays 256 (2.2× headroom over the warm set; raising it would cost shared memory in every small container to buy nothing) and max_accelerated_files stays 20000. Symfony's performance guide recommends 32531 and that number gets copied around the PHP world — for Laravel it is a 3× over-provisioned hash table.

PHP_OPCACHE_PRELOAD makes the shipped FFI usable

The standard, chromium and dev tiers compile FFI with ffi.enable=preload, but the image offered no way to preload anything — so the only sanctioned path to FFI was closed. PHP_OPCACHE_PRELOAD and PHP_OPCACHE_PRELOAD_USER are wired in both php-fpm and php-fpm-nginx. A path that does not exist fails the container at boot naming the path, instead of PHP's startup fatal that names only the ini setting.

The new FFI recipe carries both traps found while verifying it: preloading persists declarations, not state (a static property assigned during preload is empty at request time — use FFI::load() with an FFI_SCOPE and FFI::scope() per request), and ffi.enable=preload does not gate the CLI SAPI, so a working CLI script proves nothing about what FPM allows.

Testing

The security E2E scenario now asserts these defaults hard instead of warning, and runs on every push instead of only on a manual dispatch — which is how cookie_httponly=0 shipped with a test file in the repo that would have caught it. That job now also runs on arm64: every test job was ubuntu-24.04, so the arm64 manifests we publish on every build shipped on the strength of "it compiled".

Prompted by serversideup/docker-php v5.0.0-beta1, which fixed the same session-cookie default.

Full details in CHANGELOG.md.