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]