[ 
https://issues.apache.org/jira/browse/WW-5504?focusedWorklogId=983110&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-983110
 ]

ASF GitHub Bot logged work on WW-5504:
--------------------------------------

                Author: ASF GitHub Bot
            Created on: 15/Sep/25 05:33
            Start Date: 15/Sep/25 05:33
    Worklog Time Spent: 10m 
      Work Description: Copilot commented on code in PR #1352:
URL: https://github.com/apache/struts/pull/1352#discussion_r2347914884


##########
plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/PasswordTest.java:
##########
@@ -22,15 +22,14 @@
 
 import org.apache.struts2.components.Password;
 import org.apache.struts2.components.UIBean;
+import org.apache.struts2.interceptor.csp.CspNonceSource;
+import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
 
 public class PasswordTest extends AbstractCommonAttributesTest {
 
     private Password tag;
 
     public void testRenderPassword() throws Exception {
-        super.setUp();
-        this.tag = new Password(stack, request, response);
-
         tag.setName("name");

Review Comment:
   The test methods `testRenderPassword` and `testRenderPasswordShowIt` are 
calling `super.setUp()` and creating new `Password` instances inline within the 
test methods, which is inconsistent with the pattern used in `setUp()` method. 
The inline setup should be removed since `setUp()` is properly handling the 
initialization.



##########
plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/PasswordTest.java:
##########
@@ -85,8 +83,6 @@ protected void setUpStack() {
 
     @Override
     protected UIBean getUIBean() throws Exception {
-        super.setUp();
-        this.tag = new Password(stack, request, response);
         return tag;

Review Comment:
   The test methods `testRenderPassword` and `testRenderPasswordShowIt` are 
calling `super.setUp()` and creating new `Password` instances inline within the 
test methods, which is inconsistent with the pattern used in `setUp()` method. 
The inline setup should be removed since `setUp()` is properly handling the 
initialization.



##########
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:
   Consider using `@Deprecated(since = \"6.8.0\", forRemoval = true)` 
annotation on the deprecated method instead of just the comment. This provides 
better IDE support and compile-time warnings.



##########
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:
   Consider using `@Deprecated(since = \"6.8.0\", forRemoval = true)` 
annotation on the deprecated method instead of just the comment. This provides 
better IDE support and compile-time warnings.



##########
plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/PasswordTest.java:
##########
@@ -75,7 +71,9 @@ public void testRenderPasswordShowIt() throws Exception {
 
     @Override
     protected void setUp() throws Exception {
-        //dont call base setup
+        super.setUp();
+        this.tag = new Password(stack, request, response);
+        this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));

Review Comment:
   The test methods `testRenderPassword` and `testRenderPasswordShowIt` are 
calling `super.setUp()` and creating new `Password` instances inline within the 
test methods, which is inconsistent with the pattern used in `setUp()` method. 
The inline setup should be removed since `setUp()` is properly handling the 
initialization.





Issue Time Tracking
-------------------

    Worklog Id:     (was: 983110)
    Time Spent: 2h 40m  (was: 2.5h)

> 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: 2h 40m
>  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)

Reply via email to