Files
pl_mail/frankenphp
Paul LütznerandClaude Opus 5 a4d09b8433 feat(security): a content security policy on the app's own document
CODE-REVIEW S-03. The reading frame carries a carefully built policy and the
image proxy carries one; the document holding the session — composer, settings,
admin — carried none. That is the amplifier that turned S-01 and S-02 from
"script in a contenteditable" into "take the account". With a policy both would
have been malfunctions.

Enforced in production, reported under debug, because the profiler toolbar and
VarDumper inject inline scripts nothing can nonce and they exist only when debug
is on. The always-safe directives — object-src 'none', base-uri 'none',
form-action 'self', frame-ancestors 'self' — are enforced everywhere. base-uri
is the one worth naming: an injected <base> silently re-points every relative
URL on the page, module imports included, and is invisible in the markup.

It got to "enforced" by removing the violations rather than allowing them, and
the last one was the application's own stylesheets. AssetMapper implements
`import './app.css'` as a `data:application/javascript,` module that appends a
<link> at runtime, so every stylesheet was a script as far as the browser was
concerned. Permitting them would have meant `script-src … data:`, which readmits
one of the oldest injection vectors there is and gives the directive away. Both
stylesheets are <link>ed from the layout now — which is what the AssetMapper
documentation recommends for CSS anyway, and removes a flash of unstyled content
as a side effect, since the browser no longer has to run the module graph to
discover them.

The nonce is one per request, shared by the header and the layout through a
service rather than generated at each end — two nonces is the classic way this
is got wrong. It implements ResetInterface because FrankenPHP's worker mode
keeps the container alive between requests, and a nonce that survives into the
next visitor's page is not a nonce. The spec asserts both: same value in header
and document, different value on the next request.

Also, unrelated but reported while this was in flight: the entrypoint's
symfony/skeleton branch is gone. It was stock scaffolding under a comment saying
it could be deleted, it could only fire when /app held no application, and what
it did then was fail with

    Invalid stability string "", expected one of stable, RC, beta, alpha or dev

from six containers in a restart loop — because $SYMFONY_VERSION and $STABILITY
are unset in this image. Nothing in that names the actual problem. It now exits
with a message naming both ways to reach it: running the dev override from
outside the checkout (it bind-mounts ./ to /app), or a stale local
pl_mail-dev:latest being used without the override that builds it —
compose.override.yaml.dist predicted exactly this at its `image:` line.
Reproduced against an empty /app before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 12:29:47 +02:00
..
2026-07-28 21:34:58 +02:00
2026-07-30 16:40:31 +02:00
2026-07-26 10:58:31 +02:00