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


##########
grails-doc/src/en/guide/security.adoc:
##########
@@ -31,3 +31,65 @@ Grails has a few built in safety mechanisms by default.
 * The default link:scaffolding.html[scaffolding] templates HTML escape all 
data fields when displayed
 * Grails link creating tags (link:{gspTagsRef}link.html[link], 
link:{gspTagsRef}form.html[form], link:{gspTagsRef}createLink.html[createLink], 
link:{gspTagsRef}createLinkTo.html[createLinkTo] and others) all use 
appropriate escaping mechanisms to prevent code injection
 * Grails provides <<codecs,codecs>> to let you trivially escape data when 
rendered as HTML, JavaScript and URLs to prevent injection attacks here.
+
+==== HTTP Security Headers
+
+Grails servlet web applications send a small set of browser hardening headers 
by default:
+
+[cols="1,1", options="header"]
+|===
+| Header
+| Default value
+
+| `X-Content-Type-Options`
+| `nosniff`
+
+| `X-Frame-Options`
+| `SAMEORIGIN`
+
+| `Referrer-Policy`
+| `strict-origin-when-cross-origin`
+
+| `X-XSS-Protection`
+| `0`
+|===
+
+HSTS and Content Security Policy are not enabled by default because their 
correct values depend on deployment topology and application assets.
+When HSTS is enabled, Grails sends `Strict-Transport-Security` only for secure 
requests (`request.isSecure()`).
+
+If your application is deployed behind a TLS-terminating reverse proxy (the 
typical nginx/haproxy topology), the connection between the proxy and the 
application is plain HTTP, so `request.isSecure()` is `false` unless the 
container is told to trust the proxy's forwarded headers.
+Configure the Spring Boot `server.forward-headers-strategy: framework` (or 
`native`) property so `X-Forwarded-Proto` is honored, or set HSTS at the proxy 
instead, where TLS actually terminates.
+Without one of these, enabling HSTS here is a silent no-op behind most reverse 
proxies.
+
+If Spring Security's own header-writing filter (`HeaderWriterFilter`) is on 
the classpath, this filter does not apply at all - Spring Security already 
provides its own configurable header defaults, and layering Grails' eager 
defaults underneath would let them win over an application's explicit Spring 
Security header configuration.
+
+You can disable the whole filter, disable individual headers, or override 
values from `application.yml`:
+
+[source,yaml]
+.grails-app/conf/application.yml
+----
+grails:
+    security:
+        headers:
+            enabled: true
+            frame-options:
+                value: DENY
+            referrer-policy:
+                value: no-referrer-when-downgrade
+            hsts:
+                enabled: true
+                value: max-age=31536000; includeSubDomains
+            content-security-policy:
+                enabled: true
+                value: default-src 'self'
+----
+
+Set `grails.security.headers.<header>.enabled` to `false` to omit a single 
header.
+For example, `grails.security.headers.xss-protection.enabled: false` omits 
`X-XSS-Protection`.
+If an application or another filter has already set one of these headers on 
the response, Grails leaves that value unchanged.
+
+This "already set" check can only see headers set inside the servlet container 
- it has no visibility into headers a reverse proxy adds to the response after 
it leaves the application.

Review Comment:
   The duplication problem needs a behavioral fix, not a paragraph telling 
users to turn the feature off after they discover it. This is on by default, so 
a proxy-managed deployment changes what the browser receives the moment it 
upgrades, and nothing in the app logs or tests will tell them.
   
   The application cannot see headers the proxy appends on the way out, but it 
can detect that the request came through a proxy. Signals available per 
request: `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Proto`, 
`X-Forwarded-Host`, `Via`, `X-Real-IP`. Signals available from configuration: 
`server.forward-headers-strategy` set to `framework` or `native` (needed 
because the forwarding filter or valve strips those request headers before this 
filter sees them), or an active Spring Boot `CloudPlatform` (Boot itself uses 
that signal to turn forwarded-header handling on).
   
   Suggested behavior, as a `grails.security.headers.defaults` setting with 
three values:
   
   - `auto` (default): apply the built-in default values unless the request is 
detected as proxied. When it is, send only the headers the application 
configured explicitly and log once so operators can see why.
   - `always`: apply the defaults on every response, for deployments that know 
the proxy does not touch these headers.
   - `never`: never apply the defaults, only explicitly configured headers, for 
deployments where the proxy owns the headers but detection cannot see it.
   
   Explicitly configured headers (any `grails.security.headers.<header>.*` key) 
are sent in every mode. Direct-exposed apps, the ones that most need these 
defaults, keep them. Proxied apps send exactly what they sent on 7.x unless 
they opt in, so there is no upgrade regression in either direction.
   
   One caveat worth writing into the design: a client can add a forwarded 
header to its own direct request and suppress the defaults for that one 
response. That is not a practical bypass for clickjacking or MIME sniffing, 
since the attacker does not control the victim browser's request headers, but 
it should be stated.



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