Skip to main content

PolicyExtensions

Struct PolicyExtensions 

Source
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

Source

pub fn new() -> Self

No extensions: the strict policy and Cross-Origin-Opener-Policy: same-origin.

Source

pub fn img_src(self, source: impl Into<String>) -> Self

Allow images from source too (img-src keeps 'self' data:).

Source

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.

Source

pub fn form_action(self, source: impl Into<String>) -> Self

Allow forms on this page to submit to source too (form-action keeps 'self').

Source

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.

Source

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 a Reporting-Endpoints: forma-csp="<endpoint>" header: the Reporting API (Chrome and Edge 96+, Safari 16.4+, Firefox 149+ for CSP), which POSTs application/reports+json batches, an array of {"type": "csp-violation", "body": {..}};
  • report-uri <endpoint>, for engines without report-to for CSP (Firefox before 149), which POST one application/csp-report document per violation. An engine with report-to ignores 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’s sample / script-sample holds 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”

Source

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”

Source

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”

Trait Implementations§

Source§

impl Clone for PolicyExtensions

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for PolicyExtensions

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for PolicyExtensions

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl Eq for PolicyExtensions

Source§

impl PartialEq for PolicyExtensions

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for PolicyExtensions

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more