pub struct PolicyExtensions { /* private fields */ }Expand description
What a handler may add to a page’s policy, and nothing else.
The strict policy (build_csp_header) is the floor. A page that needs
another image or frame source, a form target, or a login popup adds it
here; it cannot remove a directive, loosen script-src or style-src,
or write the policy as text. The same builder names the application’s
reporting endpoint (PolicyExtensions::report_to) and relaxes Trusted
Types on a route (PolicyExtensions::relax_trusted_types). A crate::PageResponse applies the
extensions when it is made, and an invalid one fails the page with a
PolicyError instead of reaching a header.
Each source must be one CSP source expression: 'self', a scheme
(https:, blob:, data:, wss:), or a host (cdn.example.com,
https://*.example.com:8443/images/). Nonces, hashes, 'none',
'unsafe-*', 'strict-dynamic', a bare *, and anything carrying ;,
,, a quote or whitespace are refused: each of those either loosens script
execution or injects a directive. So are the javascript and vbscript
schemes in either form (javascript:, javascript://host), and data: and
filesystem: as a frame or form target.
use forma_server::PolicyExtensions;
// A console showing pasted logo URLs and a same-origin preview frame.
let console = PolicyExtensions::new().img_src("https:").frame_src("'self'");
// A login page posting to an identity provider in a popup.
let login = PolicyExtensions::new()
.form_action("https://idp.example.com")
.allow_popups();Verified by: crates/forma-server/tests/internal_renderer/page_response.rs > “no_policy_extension_yields_a_response_without_the_policy”
Implementations§
Source§impl PolicyExtensions
impl PolicyExtensions
Sourcepub fn new() -> Self
pub fn new() -> Self
No extensions: the strict policy and Cross-Origin-Opener-Policy: same-origin.
Sourcepub fn img_src(self, source: impl Into<String>) -> Self
pub fn img_src(self, source: impl Into<String>) -> Self
Allow images from source too (img-src keeps 'self' data:).
Sourcepub fn frame_src(self, source: impl Into<String>) -> Self
pub fn frame_src(self, source: impl Into<String>) -> Self
Allow frames from source. The strict policy names no frame-src, so
frames fall back to default-src 'none'; the first extension adds the
directive.
Sourcepub fn form_action(self, source: impl Into<String>) -> Self
pub fn form_action(self, source: impl Into<String>) -> Self
Allow forms on this page to submit to source too (form-action
keeps 'self').
Sourcepub fn allow_popups(self) -> Self
pub fn allow_popups(self) -> Self
Send Cross-Origin-Opener-Policy: same-origin-allow-popups instead of
same-origin, so a popup this page opens (an OAuth login) keeps its
window.opener. The page itself stays isolated from its own opener.
Sourcepub fn report_to(self, endpoint: impl Into<String>) -> Self
pub fn report_to(self, endpoint: impl Into<String>) -> Self
Send this page’s policy violations to endpoint, an endpoint the
application runs, for pages served over HTTPS. Name it
once, on the PolicyExtensions every route starts from, so no page
leaves without it.
endpoint is a path on the application’s own origin (/csp-reports,
the usual choice), an https:// URL, or, in a development build only
(cfg(debug_assertions)), an http:// URL on localhost,
127.0.0.1 or [::1]: deployed, a loopback endpoint would send each
visitor’s reports to the visitor’s own machine, so a release build
refuses it like any invalid endpoint. An absolute URL’s authority is
a host (letters, digits, . and -, or a bracketed IPv6 address) and
an optional port of digits, never userinfo: a credential written there
would go to every visitor. It is written into both policies,
the enforced one and the Trusted Types Report-Only one, twice over:
report-to forma-csp, with aReporting-Endpoints: forma-csp="<endpoint>"header: the Reporting API (Chrome and Edge 96+, Safari 16.4+, Firefox 149+ for CSP), which POSTsapplication/reports+jsonbatches, an array of{"type": "csp-violation", "body": {..}};report-uri <endpoint>, for engines withoutreport-tofor CSP (Firefox before 149), which POST oneapplication/csp-reportdocument per violation. An engine withreport-toignores it.
So the endpoint accepts both bodies. A cross-origin endpoint must also
answer CORS preflights; a same-origin path needs none. Browsers send
reports outside the page, so the policy’s connect-src does not apply.
Over plain HTTP, http://localhost included, use
PolicyExtensions::report_uri. Chrome and Firefox take
Reporting-Endpoints only from an HTTPS page, and once a policy names
report-to they ignore report-uri, so a page served over plain HTTP
with both reports nothing to the endpoint. That holds on
http://localhost too, although it is a secure context, so in
development name the endpoint with report_uri. Measured with Chrome
155 and 156 and Firefox Nightly 155 (this PR and its review): the same
page and endpoint over HTTP, on localhost or another host, sent no
report; over HTTPS each sent one application/reports+json report and
no report-uri copy.
The endpoint’s obligations are the application’s:
- Store each report’s URLs (
documentURL/document-uri,blockedURL/blocked-uri,referrer,sourceFile) without their query strings or fragments: they can carry tokens and personal data. A Trusted Types report’ssample/script-sampleholds the first characters of the value written to the sink, so treat it as personal data too. - Bound each report’s size (refuse a body over, say, 64 KiB) and the rate it accepts (per client and in total): anyone can POST to it.
- Alert per route and directive above a baseline, deduplicated: never a ticket per report. One page in a loop sends thousands.
Trusted Types enforcement does not depend on an endpoint. The additional
diagnostic Report-Only header goes out in development builds and whenever
a route explicitly relaxes enforcement. Naming an endpoint sends
it in every build. An endpoint that is empty, carries ;,
,, a quote, a backslash, whitespace, a fragment or a non-ASCII byte,
starts with //, or uses another scheme fails the page with a
PolicyError for report-to. The last of report_to and
report_uri called is the one written.
use forma_server::PolicyExtensions;
// At start: every route's policy starts from this one.
let base = PolicyExtensions::new().report_to("/csp-reports");
// A route that also needs a frame source.
let preview = base.clone().frame_src("'self'");Verified by: crates/forma-server/tests/trusted_types_report_only.rs > “a_named_endpoint_is_written_into_both_policies_and_reporting_endpoints”
Sourcepub fn report_uri(self, endpoint: impl Into<String>) -> Self
pub fn report_uri(self, endpoint: impl Into<String>) -> Self
Send this page’s policy violations to endpoint through report-uri
alone, for pages served over plain HTTP: a development server on
http://localhost, a CI run. Every current engine sends a
report-uri report from an HTTP page (one application/csp-report
POST per violation), where Chrome and Firefox send none for
PolicyExtensions::report_to’s pair, on http://localhost too. The
same endpoint rules and obligations apply; a refusal is a
PolicyError for report-uri.
use forma_server::PolicyExtensions;
let served_over_https = false; // the application's own setting
let base = if served_over_https {
PolicyExtensions::new().report_to("/csp-reports")
} else {
PolicyExtensions::new().report_uri("/csp-reports")
};Verified by: crates/forma-server/tests/trusted_types_report_only.rs > “a_named_endpoint_is_written_into_both_policies_and_reporting_endpoints”
Sourcepub fn relax_trusted_types(self, reason: impl Into<String>) -> Self
pub fn relax_trusted_types(self, reason: impl Into<String>) -> Self
Relax Trusted Types on this route, for a page whose string sinks have
not moved to trustedHtml yet (the bridge to 3.0.0’s enforcement). An
allowlisted exit: each call belongs on the application’s raw-entry
allowlist (forma-raw-lint) with an owner and an end date, and
reason should name them.
The selected route omits the two Trusted Types enforcement directives,
retaining them in Report-Only even without a reporting endpoint. Other
routes keep enforcement. Where a reporting endpoint is configured,
its violations are still reported, so the sinks left to move stay
visible. A relaxed route is logged, once per route and reason per
process, as a tracing warning naming the route and reason;
route_pattern must be the route’s pattern (/users/:id), not a
request’s path, and the log stops naming new pairs past 1,024. An
empty reason fails the page with a PolicyError for
trusted-types.
use forma_server::PolicyExtensions;
let studio = PolicyExtensions::new()
.report_to("/csp-reports")
.relax_trusted_types("app.ts writes innerHTML in four places; owner: studio, until 2027-01-31");Verified by: crates/forma-server/tests/trusted_types_report_only.rs > “a_relaxed_route_keeps_reporting_and_is_logged_once_per_route_and_reason”