Skip to content

Commit 67db131

Browse files
authored
Allow entirely disabling CSP headers (jenkinsci#23915)
Co-authored-by: Daniel Beck <[email protected]>
1 parent d35c3da commit 67db131

File tree

8 files changed

+70
-16
lines changed

8 files changed

+70
-16
lines changed

core/src/main/java/jenkins/security/csp/CspHeader.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
package jenkins.security.csp;
2626

27+
import edu.umd.cs.findbugs.annotations.CheckForNull;
2728
import org.kohsuke.accmod.Restricted;
2829
import org.kohsuke.accmod.restrictions.Beta;
2930

@@ -35,14 +36,16 @@
3536
@Restricted(Beta.class)
3637
public enum CspHeader {
3738
ContentSecurityPolicy("Content-Security-Policy"),
38-
ContentSecurityPolicyReportOnly("Content-Security-Policy-Report-Only");
39+
ContentSecurityPolicyReportOnly("Content-Security-Policy-Report-Only"),
40+
None(null);
3941

4042
private final String headerName;
4143

4244
CspHeader(String headerName) {
4345
this.headerName = headerName;
4446
}
4547

48+
@CheckForNull
4649
public String getHeaderName() {
4750
return headerName;
4851
}

core/src/main/java/jenkins/security/csp/impl/CspDecorator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,11 @@ public String getReportingEndpointsHeaderValue(HttpServletRequest req) {
105105
}
106106

107107
/**
108-
* Determines the name of the HTTP header to set.
108+
* Determines the name of the HTTP header to set, or {@code null} if none.
109109
*
110110
* @return the name of the HTTP header to set.
111111
*/
112+
@CheckForNull
112113
public String getContentSecurityPolicyHeaderName() {
113114
final Optional<CspHeaderDecider> decider = CspHeaderDecider.getCurrentDecider();
114115
if (decider.isPresent()) {

core/src/main/java/jenkins/security/csp/impl/CspFilter.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
6161

6262
CspDecorator cspDecorator = ExtensionList.lookupSingleton(CspDecorator.class);
6363
final String headerName = cspDecorator.getContentSecurityPolicyHeaderName();
64+
final boolean headerShouldBeSet = headerName != null;
6465

6566
// This is the preliminary value outside Stapler request handling (and providing a context object)
6667
final String headerValue = cspDecorator.getContentSecurityPolicyHeaderValue(req);
6768

6869
final boolean isResourceRequest = ResourceDomainConfiguration.isResourceRequest(req);
69-
if (!isResourceRequest) {
70+
71+
if (headerShouldBeSet && !isResourceRequest) {
7072
// The Filter/Decorator approach needs us to "set" headers rather than "add", so no additional endpoints are supported at the moment.
7173
final String reportingEndpoints = cspDecorator.getReportingEndpointsHeaderValue(req);
7274
if (reportingEndpoints != null) {
@@ -78,14 +80,16 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
7880
try {
7981
chain.doFilter(req, rsp);
8082
} finally {
81-
try {
82-
final String actualHeader = rsp.getHeader(headerName);
83-
if (!isResourceRequest && hasUnexpectedDifference(headerValue, actualHeader)) {
84-
LOGGER.log(Level.FINE, "CSP header has unexpected differences: Expected '" + headerValue + "' but got '" + actualHeader + "'");
83+
if (headerShouldBeSet) {
84+
try {
85+
final String actualHeader = rsp.getHeader(headerName);
86+
if (!isResourceRequest && hasUnexpectedDifference(headerValue, actualHeader)) {
87+
LOGGER.log(Level.FINE, "CSP header has unexpected differences: Expected '" + headerValue + "' but got '" + actualHeader + "'");
88+
}
89+
} catch (RuntimeException e) {
90+
// Be defensive just in case
91+
LOGGER.log(Level.FINER, "Error checking CSP header after request processing", e);
8592
}
86-
} catch (RuntimeException e) {
87-
// Be defensive just in case
88-
LOGGER.log(Level.FINER, "Error checking CSP header after request processing", e);
8993
}
9094
}
9195
}

core/src/main/java/jenkins/security/csp/impl/SystemPropertyHeaderDecider.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
package jenkins.security.csp.impl;
2626

2727
import hudson.Extension;
28+
import hudson.Util;
2829
import java.util.Arrays;
30+
import java.util.Objects;
2931
import java.util.Optional;
3032
import java.util.logging.Level;
3133
import java.util.logging.Logger;
@@ -50,7 +52,8 @@ public Optional<CspHeader> decide() {
5052
final String systemProperty = SystemProperties.getString(SYSTEM_PROPERTY_NAME);
5153
if (systemProperty != null) {
5254
LOGGER.log(Level.FINEST, "Using system property: {0}", new Object[]{ systemProperty });
53-
return Arrays.stream(CspHeader.values()).filter(h -> h.getHeaderName().equals(systemProperty)).findFirst();
55+
final String expected = Util.fixEmptyAndTrim(systemProperty);
56+
return Arrays.stream(CspHeader.values()).filter(h -> Objects.equals(expected, h.getHeaderName())).findFirst();
5457
}
5558
return Optional.empty();
5659
}

core/src/main/resources/jenkins/security/csp/impl/CspDecorator/httpHeaders.jelly

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ THE SOFTWARE.
2323
-->
2424
<?jelly escape-by-default='true'?>
2525
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
26-
<st:setHeader name="${it.getContentSecurityPolicyHeaderName()}" value="${it.getContentSecurityPolicyHeaderValue(request2)}" />
27-
<st:setHeader name="Reporting-Endpoints" value="${it.getReportingEndpointsHeaderValue(request2)}" />
26+
<j:set var="cspHeaderName" value="${it.getContentSecurityPolicyHeaderName()}"/>
27+
<j:if test="${cspHeaderName != null}">
28+
<st:setHeader name="${cspHeaderName}" value="${it.getContentSecurityPolicyHeaderValue(request2)}" />
29+
<st:setHeader name="Reporting-Endpoints" value="${it.getReportingEndpointsHeaderValue(request2)}" />
30+
</j:if>
2831
</j:jelly>

core/src/main/resources/jenkins/security/csp/impl/SystemPropertyHeaderDecider/message.jelly

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,14 @@ THE SOFTWARE.
2323
-->
2424
<?jelly escape-by-default='true'?>
2525
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
26-
<f:description>${%blurb(it.headerName)}</f:description>
26+
<f:description>
27+
<j:choose>
28+
<j:when test="${it.headerName != null}">
29+
${%blurb(it.headerName)}
30+
</j:when>
31+
<j:otherwise>
32+
${%blurbUnset}
33+
</j:otherwise>
34+
</j:choose>
35+
</f:description>
2736
</j:jelly>
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
blurb = Content Security Policy is configured to use the HTTP header <code>{0}</code> based on the system property <code>jenkins.security.csp.CspHeader.headerName</code>. \
2-
It cannot be configured through the UI while this system property specifies a header name.
2+
It cannot be configured through the UI while this system property is set.
3+
blurbUnset = Content Security Policy is disabled based on the system property <code>jenkins.security.csp.CspHeader.headerName</code>. \
4+
It cannot be configured through the UI while this system property is set.

test/src/test/java/jenkins/security/csp/impl/CspHeaderDeciderTest.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,31 @@ public void testDefaultWithSystemPropertyUnenforce(JenkinsRule j) throws IOExcep
104104
}
105105
}
106106

107+
@Test
108+
public void testDefaultWithSystemPropertyNone(JenkinsRule j) throws IOException, SAXException {
109+
System.setProperty(SystemPropertyHeaderDecider.SYSTEM_PROPERTY_NAME, "");
110+
try (JenkinsRule.WebClient webClient = j.createWebClient()) {
111+
final Optional<CspHeaderDecider> decider = CspHeaderDecider.getCurrentDecider();
112+
assertTrue(decider.isPresent());
113+
assertThat(decider.get(), instanceOf(SystemPropertyHeaderDecider.class));
114+
115+
assertFalse(ExtensionList.lookupSingleton(CspRecommendation.class).isActivated());
116+
117+
final HtmlPage htmlPage = webClient.goTo("configureSecurity");
118+
assertThat(
119+
htmlPage.getWebResponse().getContentAsString(),
120+
hasMessage(jellyResource(SystemPropertyHeaderDecider.class, "message.properties"), "blurbUnset"));
121+
assertThat(htmlPage.getWebResponse().getResponseHeaderValue("Content-Security-Policy"), nullValue());
122+
assertThat(htmlPage.getWebResponse().getResponseHeaderValue("Content-Security-Policy-Report-Only"), nullValue());
123+
124+
// submit form and confirm this didn't create a config file
125+
htmlPage.getFormByName("config").submit(htmlPage.getFormByName("config").getButtonByName("Submit"));
126+
assertFalse(ExtensionList.lookupSingleton(CspConfiguration.class).getConfigFile().exists());
127+
} finally {
128+
System.clearProperty(SystemPropertyHeaderDecider.SYSTEM_PROPERTY_NAME);
129+
}
130+
}
131+
107132
@Test
108133
public void testDefaultWithSystemPropertyWrong(JenkinsRule j) throws IOException, SAXException {
109134
System.setProperty(SystemPropertyHeaderDecider.SYSTEM_PROPERTY_NAME, "Some-Other-Value");
@@ -219,7 +244,11 @@ public void testFallbackAdminMonitorAndSetup(JenkinsRule j) throws IOException,
219244
}
220245

221246
private static Matcher<String> hasBlurb(Properties props) {
222-
return containsString(props.getProperty("blurb"));
247+
return hasMessage(props, "blurb");
248+
}
249+
250+
private static Matcher<String> hasMessage(Properties props, String key) {
251+
return containsString(props.getProperty(key));
223252
}
224253

225254
private static Properties jellyResource(Class<?> clazz, String filename) throws IOException {

0 commit comments

Comments
 (0)