Skip to main content

Content Security Policy

Content Security Policy (CSP) is a browser security mechanism that lets a server declare which content sources a page may load and execute, mitigating cross-site-scripting (XSS) and data-injection attacks. Juneau auto-generates HTML with inline <style>/<script> blocks (the HtmlDoc serializer, the Swagger UI, the OpenAPI UI, and menu-item widgets), so a CSP is the standard way to lock those pages down.

Juneau models a policy as a first-class, immutable HTTP header bean, ContentSecurityPolicy, assembled with a fluent ContentSecurityPolicy.Builder. Both live in juneau-rest-common alongside the other modern HTTP header beans.

note

CSP is off by default. Turning a policy on silently would break existing apps, so you must opt in.

Building a policy​

The builder exposes typed setters for the common directives plus a generic directive(name, sources...) escape hatch so new directives never require an API change. Source-expression keywords are exposed as constants (SELF, NONE, UNSAFE_INLINE, UNSAFE_EVAL, STRICT_DYNAMIC) and helpers (nonce(token), scheme(scheme), hash(alg, b64)).

import static org.apache.juneau.http.header.ContentSecurityPolicy.Builder.*;

String value = ContentSecurityPolicy.create()
.defaultSrc(SELF)
.scriptSrc(SELF, scheme("https"))
.imgSrc(SELF, "data:")
.objectSrc(NONE)
.baseUri(SELF)
.frameAncestors(SELF)
.build();

// => "default-src 'self'; script-src 'self' https:; img-src 'self' data:;
// object-src 'none'; base-uri 'self'; frame-ancestors 'self'"

Setting the same directive twice replaces its source list (while preserving its original position), and a directive with no sources renders as a bare keyword (e.g. directive("upgrade-insecure-requests")).

Emitting the header​

Because ContentSecurityPolicy is an ordinary string header, it flows through the existing response-header path. The simplest opt-in is the static string form on the REST annotation:

@Rest(
defaultResponseHeaders={
"Content-Security-Policy: default-src 'self'; object-src 'none'"
}
)
public class MyResource extends BasicRestServlet { ... }

For programmatic assembly, contribute the bean through a defaultResponseHeaders HttpHeaderList factory method:

@Rest(...)
public class MyResource extends BasicRestServlet {

@Bean(name="defaultResponseHeaders")
public HttpHeaderList responseHeaders() {
return HttpHeaderList.of(
ContentSecurityPolicy.create()
.defaultSrc(SELF)
.objectSrc(NONE)
.toHeader()
);
}
}

toHeader() returns a ContentSecurityPolicy whose name is Content-Security-Policy (or Content-Security-Policy-Report-Only when report-only mode is enabled).

Report-only mode​

Operators can deploy a policy in observe-only mode first, watch violations, tune, then enforce. reportOnly() flips the header name to Content-Security-Policy-Report-Only; the reportUri(...) / reportTo(...) directives point a browser at a violation collector (the collector endpoint itself is out of scope — point at your own).

ContentSecurityPolicy csp = ContentSecurityPolicy.create()
.defaultSrc(SELF)
.reportTo("csp-endpoint")
.reportOnly()
.toHeader();

// Header name => "Content-Security-Policy-Report-Only"

The strict-starter preset​

strictStarter() returns a builder pre-populated with a ready, nonce-based baseline:

ContentSecurityPolicy.strictStarter().build();

// => "default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self' 'nonce-{nonce}';
// img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"

Because Juneau's built-in pages emit inline <script>/<style>, a strict 'self'-only policy needs a per-request nonce to allow those inline blocks. The preset embeds a NONCE_PLACEHOLDER token ({nonce}); the REST layer mints one cryptographically-random nonce per response, rewrites the placeholder via resolveNonce(value, nonce), and stamps the same nonce onto every inline tag the serializer emits so the header and the markup agree.

warning

Do not combine a nonce with 'unsafe-inline' in script-src — browsers ignore 'unsafe-inline' whenever a nonce (or hash) is present. The strict starter deliberately omits 'unsafe-inline'.

See also​