[ https://issues.apache.org/jira/browse/WW-5504?focusedWorklogId=982150&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-982150 ]
ASF GitHub Bot logged work on WW-5504: -------------------------------------- Author: ASF GitHub Bot Created on: 07/Sep/25 07:54 Start Date: 07/Sep/25 07:54 Worklog Time Spent: 10m Work Description: Copilot commented on code in PR #1174: URL: https://github.com/apache/struts/pull/1174#discussion_r2328562896 ########## core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java: ########## @@ -43,63 +45,102 @@ */ public class DefaultCspSettings implements CspSettings { - private final static Logger LOG = LogManager.getLogger(DefaultCspSettings.class); + private static final Logger LOG = LogManager.getLogger(DefaultCspSettings.class); + private static final String NONCE_KEY = "nonce"; private final SecureRandom sRand = new SecureRandom(); + private CspNonceSource nonceSource = CspNonceSource.SESSION; + protected String reportUri; protected String reportTo; // default to reporting mode protected String cspHeader = CSP_REPORT_HEADER; + @Inject(value = StrutsConstants.STRUTS_CSP_NONCE_SOURCE, required = false) + public void setNonceSource(String nonceSource) { + if (StringUtils.isBlank(nonceSource)) { + this.nonceSource = CspNonceSource.SESSION; + } else { + this.nonceSource = CspNonceSource.valueOf(nonceSource.toUpperCase()); + } + } + @Override public void addCspHeaders(HttpServletRequest request, HttpServletResponse response) { + if (this.nonceSource == CspNonceSource.SESSION) { + addCspHeadersWithSession(request, response); + } else if (this.nonceSource == CspNonceSource.REQUEST) { + addCspHeadersWithRequest(request, response); + } else { + LOG.warn("Unknown nonce source: {}, ignoring CSP settings", nonceSource); + } + } + + private void addCspHeadersWithSession(HttpServletRequest request, HttpServletResponse response) { if (isSessionActive(request)) { LOG.trace("Session is active, applying CSP settings"); - associateNonceWithSession(request); - response.setHeader(cspHeader, createPolicyFormat(request)); + String nonceValue = generateNonceValue(); + request.getSession().setAttribute(NONCE_KEY, nonceValue); + response.setHeader(cspHeader, createPolicyFormat(nonceValue)); } else { - LOG.trace("Session is not active, ignoring CSP settings"); + LOG.debug("Session is not active, ignoring CSP settings"); } } + private void addCspHeadersWithRequest(HttpServletRequest request, HttpServletResponse response) { + String nonceValue = generateNonceValue(); + request.setAttribute(NONCE_KEY, nonceValue); + response.setHeader(cspHeader, createPolicyFormat(nonceValue)); + } + private boolean isSessionActive(HttpServletRequest request) { return request.getSession(false) != null; } - private void associateNonceWithSession(HttpServletRequest request) { - String nonceValue = Base64.getUrlEncoder().encodeToString(getRandomBytes()); - request.getSession().setAttribute("nonce", nonceValue); + private String generateNonceValue() { + return Base64.getUrlEncoder().encodeToString(getRandomBytes()); } - protected String createPolicyFormat(HttpServletRequest request) { - StringBuilder policyFormatBuilder = new StringBuilder() - .append(OBJECT_SRC) - .append(format(" '%s'; ", NONE)) - .append(SCRIPT_SRC) - .append(" 'nonce-%s' ") // nonce placeholder - .append(format("'%s' ", STRICT_DYNAMIC)) - .append(format("%s %s; ", HTTP, HTTPS)) - .append(BASE_URI) - .append(format(" '%s'; ", NONE)); + protected String createPolicyFormat(String nonceValue) { + StringBuilder builder = new StringBuilder() + .append(OBJECT_SRC) + .append(format(" '%s'; ", NONE)) + .append(SCRIPT_SRC) + .append(format(" 'nonce-%s' ", nonceValue)) + .append(format("'%s' ", STRICT_DYNAMIC)) + .append(format("%s %s; ", HTTP, HTTPS)) + .append(BASE_URI) + .append(format(" '%s'; ", NONE)); if (reportUri != null) { - policyFormatBuilder - .append(REPORT_URI) - .append(format(" %s; ", reportUri)); - if(reportTo != null) { - policyFormatBuilder + builder + .append(REPORT_URI) + .append(format(" %s; ", reportUri)); + if (reportTo != null) { + builder .append(REPORT_TO) .append(format(" %s; ", reportTo)); } } - return format(policyFormatBuilder.toString(), getNonceString(request)); + return builder.toString(); + } + + /** + * @deprecated since 6.8.0, for removal + */ + @Deprecated + protected String createPolicyFormat(HttpServletRequest request) { + throw new UnsupportedOperationException("Unsupported implementation, use createPolicyFormat(String) instead!"); Review Comment: The deprecated method throws an UnsupportedOperationException immediately, which creates a breaking change for any existing subclasses that override this method. Consider providing a default implementation that extracts the nonce from the request and calls the new method, or document this as a breaking change. ```suggestion // Default implementation: extract nonce from request attribute "cspNonce" Object nonceObj = request.getAttribute("cspNonce"); String nonceValue = (nonceObj instanceof String) ? (String) nonceObj : null; if (nonceValue == null) { // Fallback: generate a new nonce if not present nonceValue = generateNonceValue(); } return createPolicyFormat(nonceValue); ``` ########## core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java: ########## @@ -43,63 +45,102 @@ */ public class DefaultCspSettings implements CspSettings { - private final static Logger LOG = LogManager.getLogger(DefaultCspSettings.class); + private static final Logger LOG = LogManager.getLogger(DefaultCspSettings.class); + private static final String NONCE_KEY = "nonce"; private final SecureRandom sRand = new SecureRandom(); + private CspNonceSource nonceSource = CspNonceSource.SESSION; + protected String reportUri; protected String reportTo; // default to reporting mode protected String cspHeader = CSP_REPORT_HEADER; + @Inject(value = StrutsConstants.STRUTS_CSP_NONCE_SOURCE, required = false) + public void setNonceSource(String nonceSource) { + if (StringUtils.isBlank(nonceSource)) { + this.nonceSource = CspNonceSource.SESSION; + } else { + this.nonceSource = CspNonceSource.valueOf(nonceSource.toUpperCase()); + } + } + @Override public void addCspHeaders(HttpServletRequest request, HttpServletResponse response) { + if (this.nonceSource == CspNonceSource.SESSION) { + addCspHeadersWithSession(request, response); + } else if (this.nonceSource == CspNonceSource.REQUEST) { + addCspHeadersWithRequest(request, response); + } else { + LOG.warn("Unknown nonce source: {}, ignoring CSP settings", nonceSource); + } + } + + private void addCspHeadersWithSession(HttpServletRequest request, HttpServletResponse response) { if (isSessionActive(request)) { LOG.trace("Session is active, applying CSP settings"); - associateNonceWithSession(request); - response.setHeader(cspHeader, createPolicyFormat(request)); + String nonceValue = generateNonceValue(); + request.getSession().setAttribute(NONCE_KEY, nonceValue); + response.setHeader(cspHeader, createPolicyFormat(nonceValue)); } else { - LOG.trace("Session is not active, ignoring CSP settings"); + LOG.debug("Session is not active, ignoring CSP settings"); } } + private void addCspHeadersWithRequest(HttpServletRequest request, HttpServletResponse response) { + String nonceValue = generateNonceValue(); + request.setAttribute(NONCE_KEY, nonceValue); + response.setHeader(cspHeader, createPolicyFormat(nonceValue)); + } + private boolean isSessionActive(HttpServletRequest request) { return request.getSession(false) != null; } - private void associateNonceWithSession(HttpServletRequest request) { - String nonceValue = Base64.getUrlEncoder().encodeToString(getRandomBytes()); - request.getSession().setAttribute("nonce", nonceValue); + private String generateNonceValue() { + return Base64.getUrlEncoder().encodeToString(getRandomBytes()); } - protected String createPolicyFormat(HttpServletRequest request) { - StringBuilder policyFormatBuilder = new StringBuilder() - .append(OBJECT_SRC) - .append(format(" '%s'; ", NONE)) - .append(SCRIPT_SRC) - .append(" 'nonce-%s' ") // nonce placeholder - .append(format("'%s' ", STRICT_DYNAMIC)) - .append(format("%s %s; ", HTTP, HTTPS)) - .append(BASE_URI) - .append(format(" '%s'; ", NONE)); + protected String createPolicyFormat(String nonceValue) { + StringBuilder builder = new StringBuilder() + .append(OBJECT_SRC) + .append(format(" '%s'; ", NONE)) + .append(SCRIPT_SRC) + .append(format(" 'nonce-%s' ", nonceValue)) + .append(format("'%s' ", STRICT_DYNAMIC)) + .append(format("%s %s; ", HTTP, HTTPS)) + .append(BASE_URI) + .append(format(" '%s'; ", NONE)); if (reportUri != null) { - policyFormatBuilder - .append(REPORT_URI) - .append(format(" %s; ", reportUri)); - if(reportTo != null) { - policyFormatBuilder + builder + .append(REPORT_URI) + .append(format(" %s; ", reportUri)); + if (reportTo != null) { + builder .append(REPORT_TO) .append(format(" %s; ", reportTo)); } } - return format(policyFormatBuilder.toString(), getNonceString(request)); + return builder.toString(); + } + + /** + * @deprecated since 6.8.0, for removal + */ + @Deprecated + protected String createPolicyFormat(HttpServletRequest request) { + throw new UnsupportedOperationException("Unsupported implementation, use createPolicyFormat(String) instead!"); } + /** + * @deprecated since 6.8.0, for removal + */ + @Deprecated protected String getNonceString(HttpServletRequest request) { - Object nonce = request.getSession().getAttribute("nonce"); - return Objects.toString(nonce); + throw new UnsupportedOperationException("Unsupported implementation, don't use!"); } Review Comment: Similarly, this deprecated method throws an UnsupportedOperationException immediately, creating a potential breaking change. Consider providing a bridge implementation or clearly documenting the breaking change in release notes. Issue Time Tracking ------------------- Worklog Id: (was: 982150) Time Spent: 1.5h (was: 1h 20m) > CSP Nonce changes within a page > ------------------------------- > > Key: WW-5504 > URL: https://issues.apache.org/jira/browse/WW-5504 > Project: Struts 2 > Issue Type: Bug > Components: Core Interceptors > Affects Versions: 6.7.0 > Reporter: Andreas Sachs > Assignee: Lukasz Lenart > Priority: Major > Fix For: 6.8.0, 7.1.0 > > Time Spent: 1.5h > Remaining Estimate: 0h > > Sometimes the CSP nonce changes within a page. > > <script type="text/javascript" src="..." nonce="A"> </script> > <script type="text/javascript" src="..." nonce="A"> </script> > ... > <script type="text/javascript" src="..." nonce="B"> </script> > > This happens if there are concurrent requests within the same session. > > Each request stores a new nonce in the session: > > DefaultCspSettings: > request.getSession().setAttribute("nonce", nonceValue); > > If the first request is not finished, the second request will change the > nonce of the first request. > > > > -- This message was sent by Atlassian Jira (v8.20.10#820010)