Skip to content

Commit f7cf283

Browse files
jvzdaniel-beck
authored andcommitted
1 parent f479652 commit f7cf283

File tree

4 files changed

+172
-1
lines changed

4 files changed

+172
-1
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package jenkins.security;
2+
3+
import jenkins.util.SystemProperties;
4+
import org.kohsuke.accmod.Restricted;
5+
import org.kohsuke.accmod.restrictions.NoExternalUse;
6+
7+
import javax.servlet.Filter;
8+
import javax.servlet.FilterChain;
9+
import javax.servlet.FilterConfig;
10+
import javax.servlet.ServletException;
11+
import javax.servlet.ServletRequest;
12+
import javax.servlet.ServletResponse;
13+
import javax.servlet.http.HttpServletRequest;
14+
import javax.servlet.http.HttpServletResponse;
15+
import java.io.IOException;
16+
import java.util.logging.Logger;
17+
18+
@Restricted(NoExternalUse.class)
19+
public class SuspiciousRequestFilter implements Filter {
20+
21+
/** System property name set to true or false to indicate whether or not semicolons should be allowed in URL paths. */
22+
public static final String ALLOW_SEMICOLONS_IN_PATH = SuspiciousRequestFilter.class.getName() + ".allowSemicolonsInPath";
23+
public static boolean allowSemicolonsInPath = SystemProperties.getBoolean(ALLOW_SEMICOLONS_IN_PATH, false);
24+
private static final Logger LOGGER = Logger.getLogger(SuspiciousRequestFilter.class.getName());
25+
26+
@Override
27+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
28+
HttpServletRequest httpRequest = (HttpServletRequest) request;
29+
HttpServletResponse httpResponse = (HttpServletResponse) response;
30+
if (!allowSemicolonsInPath && httpRequest.getRequestURI().contains(";")) {
31+
LOGGER.warning(() -> "Denying HTTP " + httpRequest.getMethod() + " to " + httpRequest.getRequestURI() +
32+
" as it has an illegal semicolon in the path. This behavior can be overridden by setting the system property " +
33+
ALLOW_SEMICOLONS_IN_PATH + " to true. For more information, see https://jenkins.io/redirect/semicolons-in-urls");
34+
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "Semicolons are not allowed in the request URI");
35+
} else {
36+
chain.doFilter(request, response);
37+
}
38+
}
39+
40+
@Override
41+
public void init(FilterConfig filterConfig) throws ServletException {
42+
}
43+
44+
@Override
45+
public void destroy() {
46+
}
47+
}

test/src/test/java/hudson/security/csrf/CrumbExclusionTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@
3636
import jenkins.model.Jenkins;
3737
import static org.hamcrest.Matchers.containsString;
3838

39+
import jenkins.security.SuspiciousRequestFilter;
40+
import org.junit.AfterClass;
3941
import org.junit.Assert;
42+
import org.junit.BeforeClass;
4043
import org.junit.Test;
4144
import static org.junit.Assert.*;
4245
import org.junit.Rule;
@@ -57,6 +60,16 @@ public class CrumbExclusionTest {
5760
@Rule
5861
public JenkinsRule r = new JenkinsRule();
5962

63+
@BeforeClass
64+
public static void prepare() {
65+
SuspiciousRequestFilter.allowSemicolonsInPath = true;
66+
}
67+
68+
@AfterClass
69+
public static void cleanup() {
70+
SuspiciousRequestFilter.allowSemicolonsInPath = false;
71+
}
72+
6073
@Issue("SECURITY-1774")
6174
@Test
6275
public void pathInfo() throws Exception {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package jenkins.security;
2+
3+
import com.gargoylesoftware.htmlunit.WebRequest;
4+
import com.gargoylesoftware.htmlunit.WebResponse;
5+
import hudson.ExtensionList;
6+
import hudson.model.UnprotectedRootAction;
7+
import org.junit.Rule;
8+
import org.junit.Test;
9+
import org.jvnet.hudson.test.Issue;
10+
import org.jvnet.hudson.test.JenkinsRule;
11+
import org.jvnet.hudson.test.TestExtension;
12+
import org.kohsuke.stapler.QueryParameter;
13+
import org.kohsuke.stapler.verb.GET;
14+
15+
import javax.annotation.CheckForNull;
16+
import javax.servlet.http.HttpServletResponse;
17+
import java.net.URL;
18+
19+
import static org.hamcrest.MatcherAssert.assertThat;
20+
import static org.hamcrest.Matchers.containsString;
21+
import static org.hamcrest.Matchers.is;
22+
import static org.hamcrest.Matchers.nullValue;
23+
24+
@Issue("SECURITY-1774")
25+
public class SuspiciousRequestFilterTest {
26+
27+
@Rule
28+
public JenkinsRule j = new JenkinsRule();
29+
30+
private WebResponse get(String path) throws Exception {
31+
return j.createWebClient()
32+
.withThrowExceptionOnFailingStatusCode(false)
33+
.getPage(new WebRequest(new URL(j.getURL(), path)))
34+
.getWebResponse();
35+
}
36+
37+
@Test
38+
public void denySemicolonInRequestPathByDefault() throws Exception {
39+
WebResponse response = get("foo/bar/..;/?baz=bruh");
40+
assertThat(Foo.getInstance().baz, is(nullValue()));
41+
assertThat(response.getStatusCode(), is(HttpServletResponse.SC_BAD_REQUEST));
42+
assertThat(response.getContentAsString(), containsString("Semicolons are not allowed in the request URI"));
43+
}
44+
45+
@Test
46+
public void allowSemicolonsInRequestPathWhenEscapeHatchEnabled() throws Exception {
47+
SuspiciousRequestFilter.allowSemicolonsInPath = true;
48+
try {
49+
WebResponse response = get("foo/bar/..;/..;/cli?baz=bruh");
50+
assertThat(Foo.getInstance().baz, is("bruh"));
51+
assertThat(response.getStatusCode(), is(HttpServletResponse.SC_OK));
52+
} finally {
53+
SuspiciousRequestFilter.allowSemicolonsInPath = false;
54+
}
55+
}
56+
57+
@Test
58+
public void allowSemicolonsInQueryParameters() throws Exception {
59+
WebResponse response = get("foo/bar?baz=foo;bar=baz");
60+
assertThat(Foo.getInstance().baz, is("foo;bar=baz"));
61+
assertThat(response.getStatusCode(), is(HttpServletResponse.SC_OK));
62+
}
63+
64+
@TestExtension
65+
public static class Foo implements UnprotectedRootAction {
66+
67+
private static Foo getInstance() {
68+
return ExtensionList.lookupSingleton(Foo.class);
69+
}
70+
71+
private String baz;
72+
73+
@CheckForNull
74+
@Override
75+
public String getIconFileName() {
76+
return null;
77+
}
78+
79+
@CheckForNull
80+
@Override
81+
public String getDisplayName() {
82+
return "Pitied Foos";
83+
}
84+
85+
@CheckForNull
86+
@Override
87+
public String getUrlName() {
88+
return "foo";
89+
}
90+
91+
@GET
92+
public void doBar(@QueryParameter String baz) {
93+
this.baz = baz;
94+
}
95+
96+
@GET
97+
public void doIndex(@QueryParameter String baz) {
98+
this.baz = "index: " + baz;
99+
}
100+
}
101+
102+
}

war/src/main/webapp/WEB-INF/web.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ THE SOFTWARE.
5050
<url-pattern>/*</url-pattern>
5151
</servlet-mapping>
5252

53+
<filter>
54+
<filter-name>suspicious-request-filter</filter-name>
55+
<filter-class>jenkins.security.SuspiciousRequestFilter</filter-class>
56+
<async-supported>true</async-supported>
57+
</filter>
5358
<filter>
5459
<filter-name>diagnostic-name-filter</filter-name>
5560
<filter-class>org.kohsuke.stapler.DiagnosticThreadNameFilter</filter-class>
@@ -125,7 +130,11 @@ THE SOFTWARE.
125130
<url-pattern>*.png</url-pattern>
126131
</filter-mapping>
127132
-->
128-
133+
134+
<filter-mapping>
135+
<filter-name>suspicious-request-filter</filter-name>
136+
<url-pattern>/*</url-pattern>
137+
</filter-mapping>
129138
<filter-mapping>
130139
<filter-name>diagnostic-name-filter</filter-name>
131140
<url-pattern>/*</url-pattern>

0 commit comments

Comments
 (0)