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]

Reply via email to