jdaugherty commented on code in PR #16738:
URL: https://github.com/apache/grails-core/pull/16738#discussion_r4363348870


##########
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:
   The docs promise this tag renders Spring Security's nonce as well, but no 
test puts one in front of it: `ApplicationTagLibTests` covers the Grails nonce 
and a String. Spring Security's holder is a private nested class behind a 
public interface, which is the shape Groovy's dynamic dispatch has to get right 
here (public method on a non-public declaring class). A case with 
`ContentSecurityPolicyNonceGeneratingFilter` ahead of the template, or a 
private holder class implementing a public `getNonce()` interface if 
spring-security-web is not on this module's test classpath, would pin it.



##########
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:
   `request._csp?.nonce` only compiles in a dynamically compiled controller. 
Under `@GrailsCompileStatic` there is no typed read that works for both 
producers, since a cast to 
`GrailsSecurityHeadersFilter.ContentSecurityPolicyNonce` fails when Spring 
Security generated the nonce. Either qualify the sentence, as the tag reference 
does for pages, or make the filter's `nonceOf(HttpServletRequest)` a public 
static reader and point statically compiled code at it.



##########
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:
   Spring Security's holder is 
`ContentSecurityPolicyNonceGeneratingFilter.DeferredContentSecurityPolicyNonce`,
 a private nested class; the hint in `ControllerRuntimeHints` registers only 
the `ContentSecurityPolicyNonce` interface. This lookup reflects on the 
holder's own class first (`Class.getMethod` on the private class), which has no 
metadata in a native image, so depending on the reachability mode it either 
fails to find the inherited method (policy dropped with the "has no nonce to 
read" warning) or throws `MissingReflectionRegistrationError` out of the 
commit. Reading through the registered type avoids both and is shorter:
   
   ```java
   Class<?> nonceType = ClassUtils.forName(SPRING_SECURITY_NONCE, 
holder.getClass().getClassLoader()); // guarded by ClassUtils.isPresent
   if (nonceType.isInstance(holder)) {
       Object nonce = 
ReflectionUtils.invokeMethod(nonceType.getMethod("getNonce"), holder);
       ...
   }
   ```
   
   Was the native path tried with Spring Security generating the nonce? If it 
was and it works, ignore this.



-- 
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