codeconsole commented on code in PR #16738:
URL: https://github.com/apache/grails-core/pull/16738#discussion_r4364094936
##########
grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsSecurityHeadersFilter.java:
##########
@@ -135,7 +170,57 @@ private void writeHeaders(HttpServletResponse response,
boolean applyDefaults, b
applyHeader(response, "Strict-Transport-Security", "hsts",
this.properties.getHsts(), applyDefaults);
}
applyHeader(response, "Content-Security-Policy",
"content-security-policy",
- this.properties.getContentSecurityPolicy(), applyDefaults);
+ this.properties.getContentSecurityPolicy(), applyDefaults,
(policy) -> withNonce(policy, request));
+ }
+
+ private boolean usesNonce() {
+ GrailsSecurityHeadersProperties.Header policy =
this.properties.getContentSecurityPolicy();
+ return isEnabled(policy) && policy.getValue() != null &&
policy.getValue().contains(NONCE_PLACEHOLDER);
+ }
+
+ /**
+ * The policy with each {@code {nonce}} replaced by the nonce the request
carries when the
+ * header is written, or {@code null} when the request carries none that
can be read.
+ */
+ private String withNonce(String policy, HttpServletRequest request) {
+ if (!policy.contains(NONCE_PLACEHOLDER)) {
+ return policy;
+ }
+ String nonce = nonceOf(request);
+ if (nonce == null) {
+ if (this.unreadableNonceLogged.compareAndSet(false, true)) {
+ logger.warn("The Content-Security-Policy uses {} but the {}
request attribute holds a {}, which " +
+ "has no nonce to read, so the header is not sent.",
NONCE_PLACEHOLDER, NONCE_ATTRIBUTE,
+
request.getAttribute(NONCE_ATTRIBUTE).getClass().getName());
+ }
+ return null;
+ }
+ return policy.replace(NONCE_PLACEHOLDER, nonce);
+ }
+
+ /**
+ * The nonce the request carries: the one this filter generated, or the
one Spring Security's
+ * {@code ContentSecurityPolicyNonceGeneratingFilter} set, whose {@code
getNonce()} is read
+ * through Spring Security's public interface since this module does not
depend on it.
+ */
+ private static String nonceOf(HttpServletRequest request) {
+ Object holder = request.getAttribute(NONCE_ATTRIBUTE);
+ if (holder == null) {
+ // Removed after this filter set it, so the header gets one of its
own.
+ ContentSecurityPolicyNonce nonce = new
ContentSecurityPolicyNonce();
+ request.setAttribute(NONCE_ATTRIBUTE, nonce);
+ return nonce.getNonce();
+ }
+ if (holder instanceof ContentSecurityPolicyNonce nonce) {
+ return nonce.getNonce();
+ }
+ Method getNonce = ClassUtils.getMethodIfAvailable(holder.getClass(),
"getNonce");
Review Comment:
It hadn't been tried. It has now, with GraalVM 25.2.4. The test image runs
Spring Security's real `ContentSecurityPolicyNonceGeneratingFilter` ahead of
`GrailsSecurityHeadersFilter`, with a Grails policy of `script-src
'nonce-{nonce}'` and only the reflection metadata the hints register:
| Image | 745d0a30b5 | 3bafab6002 |
|---|---|---|
| default | policy not sent | policy with Spring Security's nonce |
| `--exact-reachability-metadata-path` on grails-controllers,
grails-web-common and spring-core | policy not sent | policy with Spring
Security's nonce |
| `--exact-reachability-metadata`, with every resource registered so that
slf4j and commons-logging start | policy with Spring Security's nonce | policy
with Spring Security's nonce |
So the lookup missed the method rather than throwing. Only the fully exact
image found it.
Fixed in 3bafab6002 by reading through the registered type, as you
suggested. The reader is now
`grails.web.http.ContentSecurityPolicyNonce.nonceOf(request)` in
grails-web-common; the tag shares it, see the other two threads. It resolves
`org.springframework.security.web.header.ContentSecurityPolicyNonce` with the
holder's class loader, checks `isInstance`, and invokes `getNonce` on the
interface. `ContentSecurityPolicyRuntimeHints` registers that interface and the
Grails nonce next to the reader, in place of the two `ControllerRuntimeHints`
entries.
The reader is Java. A first, `@CompileStatic` Groovy version coerced the
result with `as String`, which goes through the metaclass registry
(`groovy.runtime.metaclass.java.lang.StringMetaClass`). The fully exact image
refused that, and the policy was dropped. The Java version passes all three
images.
One behavior change: an object under `_csp` that is neither nonce is no
longer read for a `getNonce()` of its own. The policy is not sent, and the
warning says so.
##########
grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ApplicationTagLib.groovy:
##########
@@ -123,6 +129,22 @@ class ApplicationTagLib implements
ApplicationContextAware, InitializingBean, Gr
attrs.name ? request.getHeader(attrs.name) : null
}
+ /**
+ * Renders the nonce of the request's Content-Security-Policy, for the
nonce attribute of an inline script or
+ * style, or nothing when the request carries none.
+ *
+ * @emptyTag
+ */
+ def cspNonce(Map attrs) {
+ Object holder = request.getAttribute(CSP_NONCE_ATTRIBUTE)
+ try {
+ holder?.nonce
Review Comment:
Added in 5f76fe9db8. spring-security-web is now a test-only dependency of
grails-gsp. `ApplicationTagLibTests` renders a dynamic and a statically
compiled page behind Spring Security's own
`ContentSecurityPolicyNonceGeneratingFilter`, with the Grails filter inside it.
It checks that the page and the Grails policy both name the nonce Spring
Security carries in its `DeferredContentSecurityPolicyNonce`.
The tag also no longer leaves the read to Groovy's dispatch. It calls
`ContentSecurityPolicyNonce.nonceOf(request)`, the reader the filter uses, so
the page and the header agree on what counts as a nonce.
I checked the old `holder?.nonce` read in a native image too, compiled with
invokedynamic as on 9.x. It found the getter on the private class in a default
image, and with exact metadata scoped to the probe, grails-web-common,
spring-core and groovy. So that read was not broken; the change gives the tag
and the filter one definition.
The reader lives in grails-web-common rather than on the filter because
`grails-gsp-spring-boot` gives a plain Spring Boot application the GSP tags
without grails-controllers.
##########
grails-doc/src/en/guide/security.adoc:
##########
@@ -152,3 +152,49 @@ Set `grails.security.headers.<header>.enabled` to `false`
to omit a single heade
For example, `grails.security.headers.xss-protection.enabled: false` omits
`X-XSS-Protection`.
A header that is enabled but has a blank `value` is not sent; Grails logs a
warning naming the property the first time that happens, since it usually means
`content-security-policy.enabled` was set without a policy.
If an application, another filter, or Spring Security has already set one of
these headers on the response by the time it commits, Grails leaves that value
unchanged.
+
+===== Nonce-based Content Security Policy
+
+A policy can allow an inline script or style by a nonce rather than with
`'unsafe-inline'`.
+Name the nonce in the policy with the `+{nonce}+` placeholder, which is the
placeholder Spring Security 7.2 introduced:
+
+[source,yaml]
+.grails-app/conf/application.yml
+----
+grails:
+ security:
+ headers:
+ content-security-policy:
+ enabled: true
+ value: "script-src 'self' 'nonce-{nonce}'; style-src 'self'
'nonce-{nonce}'; object-src 'none'"
+----
+
+Each request then carries a nonce of its own, 128 random bits encoded in
Base64, under the `_csp` request attribute.
+Every `+{nonce}+` in the policy is replaced with that nonce when the header is
written.
+A view gives the nonce to each inline script and style with the
link:{gspTagsRef}cspNonce.html[cspNonce] tag, which works in a statically
compiled page as well as a dynamic one:
+
+[source,xml]
+----
+<script nonce="${g.cspNonce()}">
+ document.getElementById('greeting').textContent = 'Hello'
+</script>
+<style nonce="${g.cspNonce()}">
+ #greeting { font-weight: bold }
+</style>
+----
+
+The link:{gspTagsRef}javascript.html[javascript] tag adds the nonce to the
inline script it writes.
+A controller can read the nonce as `request._csp?.nonce`.
Review Comment:
I went with the public reader:
`grails.web.http.ContentSecurityPolicyNonce.nonceOf(HttpServletRequest)`, added
in 3bafab6002 and documented in 07f1a52dec. It compiles under
`@GrailsCompileStatic` and returns `null` when the request carries no nonce.
It is a class of its own in grails-web-common rather than a static on the
filter. The tag uses it too, and a plain Spring Boot application using GSP has
the tag but not grails-controllers.
The security guide, the `cspNonce` reference and What's New now point code
outside a view at it instead of `request._csp?.nonce`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]