CSP support #12084#12085
Conversation
Codacy's Analysis Summary0 new issue (≤ 0 issue)
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #12085 +/- ##
============================================
- Coverage 86.04% 86.02% -0.03%
- Complexity 19377 19466 +89
============================================
Files 2544 2549 +5
Lines 65928 66162 +234
Branches 5289 5300 +11
============================================
+ Hits 56728 56916 +188
- Misses 6636 6678 +42
- Partials 2564 2568 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Introduce a first-class ContentSecurityPolicy model on PortalRequest, plus
a lib-portal JavaScript API (portal.getCsp()) exposing the same surface to
script code. Multiple contributors during request rendering (platform,
site app, custom apps, widgets, page controllers, parts, layouts) can
extend the same instance through add / set / addSha / nonce /
applyNonceTo. The final Content-Security-Policy header value is composed
at portal response-flush time by BasePortalHandler, so late additions
during rendering still land in the header.
Java surface (portal-api):
- com.enonic.xp.portal.csp.ContentSecurityPolicy — mutable, request-scoped;
methods return this for chaining; no separate Builder.
- com.enonic.xp.portal.csp.HashAlgo — SHA256 / SHA384 / SHA512.
- PortalRequest.getContentSecurityPolicy() — lazy field, same instance per
request.
- BasePortalHandler.doHandle — emits Content-Security-Policy from
policy.build() on both happy-path and exception-rendered paths,
preserves any header the response already carries, and keeps the
PortalResponse type via PortalResponse.create(...).
JavaScript surface (lib-portal):
- portal.getCsp() — returns a Csp object backed by the request-scoped
policy, so Java and JS contributors compose on a single instance.
- Csp.add / set / addSha / getNonce / applyNonceTo.
- The 2-arg addSha hashes UTF-8 bytes of the content; the 3-arg form
accepts a precomputed digest + algo string ('sha256' | 'sha384' |
'sha512'); unsupported algo throws.
Tests:
- ContentSecurityPolicyTest covers add/set/dedupe, addSha bytes vs
precomputed, nonce lazy + cached, applyNonceTo before / after nonce(),
alphabetical directive order, unsafe-inline + nonce coexistence, late
mutation lands in subsequent build(), null arguments throw.
- PortalRequestTest covers same-instance-per-request and
distinct-instances-across-requests.
- BasePortalHandlerTest covers header emission, no-emit when policy was
never accessed or stayed empty, preserves existing CSP header,
preserves PortalResponse type, exception-path emission.
- CspHandlerScriptTest covers the JS surface end-to-end via Nashorn.
Closes #12084
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The only realistic use case for applyNonceTo was inline <style nonce="...">, which is rare and one line via the existing add method. Drop the method to keep the CSP API minimal: the nonce is added to script-src on first nonce() call; callers wanting the nonce in other directives use add directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the stringly-typed CSP surface with typed per-directive methods plus constants for the special source/sandbox keywords, keeping add / set / addSha as escape hatches. New methods on ContentSecurityPolicy / Csp (chainable): - defaultSrc / scriptSrc / styleSrc / imgSrc / fontSrc / connectSrc / mediaSrc / objectSrc / frameSrc / workerSrc / manifestSrc / childSrc - frameAncestors / baseUri / formAction - upgradeInsecureRequests - sandbox(SandboxFlag... flags) - addScriptSrcSha / addStyleSrcSha (bytes + precomputed-digest overloads) New constants: - CspSource: SELF / NONE / UNSAFE_INLINE / UNSAFE_EVAL / STRICT_DYNAMIC / UNSAFE_HASHES / WASM_UNSAFE_EVAL / REPORT_SAMPLE -- emitted single-quoted - SandboxFlag: ALLOW_SCRIPTS / ALLOW_SAME_ORIGIN / ALLOW_FORMS / ALLOW_POPUPS / ALLOW_MODALS / ALLOW_TOP_NAVIGATION / ALLOW_DOWNLOADS / ALLOW_POINTER_LOCK / ALLOW_PRESENTATION / ALLOW_ORIENTATION_LOCK -- emitted unquoted Java directive methods come in CspSource... / String... overload pairs so host/scheme strings and typed keywords are both supported without losing type safety. TS surface uses (CspSource | string)[] variadics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed What changed
Local tests
CI on |
Follow-ups on the CSP API: - lib-portal: portal.getCsp() -> portal.csp() (drop the get- prefix); example + script test renamed to csp.js / csp-test.js accordingly. - ContentSecurityPolicy: add two one-shot strict baselines -- strict() (deny-all: default-src/base-uri/frame-ancestors 'none') and strictDynamic() (web.dev nonce + 'strict-dynamic' script policy). - Nonce: replace the script-only getNonce() with nonceScriptSrc(), nonceStyleSrc() and nonce() (both) -- the only two directives a nonce- source is valid for. One cached per-request value; each call wires its directive. strictDynamic() uses nonceScriptSrc(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Schemes are emitted verbatim (data:, blob:) vs single-quoted keywords. HTTPS intentionally omitted (no clear use-case); strictDynamic keeps its raw https: source. Updates the example and adds token + typed-scheme tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The web.dev legacy fallbacks only apply on browsers without strict-dynamic / nonce support, where they weaken the policy (any https script allowed). strictDynamic() now seeds the pure modern core: script-src 'nonce-<r>' 'strict-dynamic'; object-src 'none'; base-uri 'none' Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes #12084
Adds a first-class
ContentSecurityPolicymodel onPortalRequest, plus alib-portalJavaScript surface (portal.csp()) so site / page controllers, parts, layouts, and widgets can contribute directives, SHA hashes, and a request nonce alongside Java contributors. The finalContent-Security-Policyheader is composed at portal response-flush time inBasePortalHandler, so late additions during rendering still land in the header.JavaScript API (
lib-portal)All directive methods are chainable.
CspSourcekeyword constants are emitted single-quoted ('self','none','unsafe-inline','unsafe-eval','strict-dynamic','unsafe-hashes','wasm-unsafe-eval','report-sample'); scheme constants are emitted verbatim (data:,blob:).SandboxFlagconstants are emitted unquoted (allow-scripts,allow-same-origin, ...).Java API
PortalRequest.getContentSecurityPolicy()lazily creates a mutable, request-scopedContentSecurityPolicy(same instance for the lifetime of the request; no separateBuilder). It mirrors the JS surface:All directive methods return
ContentSecurityPolicy(chainable);build()renders the header value.Presets
strict()-- deny-all baseline:base-uri 'none'; default-src 'none'; frame-ancestors 'none'. Call it first, then open up only what you need.strictDynamic()-- the web.dev nonce-based "strict CSP":base-uri 'none'; object-src 'none'; script-src 'nonce-...' 'strict-dynamic'. The nonce is generated eagerly onscript-src; retrieve it withnonceScriptSrc().Semantics
addis union (deduped) for every directive class (source-list, boolean, restrictive);setresets a directive's source list -- there is no freeze, soaddaftersetstill extends.nonce-source is valid only forscript-srcandstyle-src;nonceScriptSrc(),nonceStyleSrc()andnonce()(both) all return the same value and wire their target directive. If nononce*method is called, nononce-entry is emitted.'unsafe-inline'and'nonce-...'may coexist -- browser precedence rules apply.Test plan
./gradlew :portal:portal-api:test :lib:lib-portal:test-- green.ContentSecurityPolicyTest(Java model) +CspHandlerScriptTest(script bridge) cover: typed/raw directives, restrictive directives, boolean, sandbox, hashes, thestrict()/strictDynamic()presets, thenonceScriptSrc/nonceStyleSrc/noncetrio, and theCspSourcekeyword + scheme tokens.Generated with Claude Code