This is an automated email from the ASF dual-hosted git repository. lprimak pushed a commit to branch 3.x in repository https://gitbox.apache.org/repos/asf/shiro.git
commit 5ed8730eaf86736922aa74eb6703969cc80da20e Author: lprimak <[email protected]> AuthorDate: Thu May 21 19:34:39 2026 -0500 enh(jakarta-ee): strip out the host part of the referer header --- .../shiro/ee/filters/FormResubmitSupport.java | 28 ++++- .../apache/shiro/ee/filters/FormSupportTest.java | 137 +++++++++++++++++++-- 2 files changed, 150 insertions(+), 15 deletions(-) diff --git a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitSupport.java b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitSupport.java index 695165a72..02c9164de 100644 --- a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitSupport.java +++ b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitSupport.java @@ -258,13 +258,31 @@ public class FormResubmitSupport { static String getReferer(HttpServletRequest request) { String referer = request.getHeader("referer"); - if (referer != null) { - // do not switch to https if custom port is specified - if (!referer.matches("^http:\\/\\/[A-z|.|[0-9]]+:[0-9]+(\\/.*|$)")) { - referer = referer.replaceFirst("^http:", "https:"); + if (referer == null || referer.isBlank()) { + return null; + } + + try { + URI uri = URI.create(referer); + + String contextPath = WebUtils.getContextPath(request); + String path = WebUtils.normalize(uri.getPath()); + + if (path == null) { + return null; } + + if (!contextPath.isEmpty() + && !path.equals(contextPath) + && !path.startsWith(contextPath + "/")) { + return null; + } + + String query = uri.getRawQuery(); + return query == null ? path : path + "?" + query; + } catch (IllegalArgumentException e) { + return null; } - return referer; } /** diff --git a/support/jakarta-ee/src/test/java/org/apache/shiro/ee/filters/FormSupportTest.java b/support/jakarta-ee/src/test/java/org/apache/shiro/ee/filters/FormSupportTest.java index 8272823d8..99dcfdb64 100644 --- a/support/jakarta-ee/src/test/java/org/apache/shiro/ee/filters/FormSupportTest.java +++ b/support/jakarta-ee/src/test/java/org/apache/shiro/ee/filters/FormSupportTest.java @@ -53,28 +53,145 @@ class FormSupportTest { assertThat(getReferer(request)).isNull(); } + @Test + void blankReferer() { + when(request.getHeader("referer")).thenReturn(" "); + assertThat(getReferer(request)).isNull(); + } + @Test void plainStringReferer() { when(request.getHeader("referer")).thenReturn("hello"); - assertThat(getReferer(request)).isEqualTo("hello"); + when(request.getContextPath()).thenReturn("/myapp"); + assertThat(getReferer(request)).isNull(); + } + + @Test + void malformedReferer() { + when(request.getHeader("referer")).thenReturn("http://exa mple.com"); + assertThat(getReferer(request)).isNull(); + } + + @Test + void refererWithinContextPath() { + when(request.getHeader("referer")).thenReturn("https://example.com/myapp/login.xhtml"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isEqualTo("/myapp/login.xhtml"); + } + + @Test + void refererWithinContextPathWithQuery() { + when(request.getHeader("referer")).thenReturn("https://example.com/myapp/login.xhtml?a=1&b=2"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isEqualTo("/myapp/login.xhtml?a=1&b=2"); } @Test - void switchToHttps() { - when(request.getHeader("referer")).thenReturn("http://example.com"); - assertThat(getReferer(request)).isEqualTo("https://example.com"); + void refererEqualToContextPathBecomesRoot() { + when(request.getHeader("referer")).thenReturn("https://example.com/myapp"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isEqualTo("/myapp"); } @Test - void dontSwitchToHttpsWhenCustomPort() { - when(request.getHeader("referer")).thenReturn("http://example.com:8080/"); - assertThat(getReferer(request)).isEqualTo("http://example.com:8080/"); + void refererOutsideContextPathIsRejected() { + when(request.getHeader("referer")).thenReturn("https://example.com/otherapp/login.xhtml"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isNull(); } @Test - void dontSwitchToHttpsWhenCustomPortNoTrailingSlash() { - when(request.getHeader("referer")).thenReturn("http://example.com:8080"); - assertThat(getReferer(request)).isEqualTo("http://example.com:8080"); + void rootContextKeepsPath() { + when(request.getHeader("referer")).thenReturn("https://example.com/login.xhtml"); + when(request.getContextPath()).thenReturn(""); + + assertThat(getReferer(request)).isEqualTo("/login.xhtml"); + } + + @Test + void rootContextKeepsPathWithQuery() { + when(request.getHeader("referer")).thenReturn("https://example.com/login.xhtml?x=1"); + when(request.getContextPath()).thenReturn(""); + + assertThat(getReferer(request)).isEqualTo("/login.xhtml?x=1"); + } + + @Test + void normalizedPathWithinContextIsAccepted() { + when(request.getHeader("referer")).thenReturn("https://example.com/myapp//foo/./bar.xhtml"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isEqualTo("/myapp/foo/bar.xhtml"); + } + + @Test + void normalizedPathEscapingContextIsRejected() { + when(request.getHeader("referer")).thenReturn("https://example.com/myapp/../otherapp/page.xhtml"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isNull(); + } + + @Test + void opaqueUriRefererIsRejected() { + when(request.getHeader("referer")).thenReturn("mailto:[email protected]"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isNull(); + } + + @Test + void javascriptUriRefererIsRejected() { + when(request.getHeader("referer")).thenReturn("javascript:alert(1)"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isNull(); + } + + @Test + void contextPathPrefixMatchRequiresPathBoundary() { + when(request.getHeader("referer")).thenReturn("https://example.com/myapplication/page.xhtml"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isNull(); + } + + @Test + void refererWithFragmentDropsFragmentAndKeepsQueryOnly() { + when(request.getHeader("referer")).thenReturn("https://example.com/myapp/page.xhtml?a=1#frag"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isEqualTo("/myapp/page.xhtml?a=1"); + } + + @Test + void externalHostWithMatchingContextCurrentlyPasses() { + when(request.getHeader("referer")).thenReturn("https://attacker.example/myapp/login.xhtml"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isEqualTo("/myapp/login.xhtml"); + } + + @Test + void encodedPathTraversalRefererIsRejected() { + when(request.getHeader("referer")) + .thenReturn("https://example.com/myapp/%2e%2e/otherapp/page.xhtml"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isNull(); + } + + @Test + void encodedPathTraversalWithEncodedSlashesRefererIsRejected() { + when(request.getHeader("referer")) + .thenReturn("https://example.com/myapp/%2e%2e%2fotherapp%2fpage.xhtml"); + when(request.getContextPath()).thenReturn("/myapp"); + + assertThat(getReferer(request)).isNull(); } @Test
