Skip to content

Commit 2ba827b

Browse files
authored
[JENKINS-76263] Add Content-Security-Policy header (#11269)
1 parent 2fa902c commit 2ba827b

File tree

61 files changed

+4239
-34
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+4239
-34
lines changed

core/src/main/java/hudson/markup/MarkupFormatter.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,8 @@
3535
import java.io.Writer;
3636
import java.util.Collections;
3737
import java.util.Map;
38-
import java.util.function.Function;
3938
import java.util.logging.Level;
4039
import java.util.logging.Logger;
41-
import java.util.stream.Collectors;
42-
import java.util.stream.Stream;
4340
import jenkins.util.SystemProperties;
4441
import org.kohsuke.accmod.Restricted;
4542
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -133,7 +130,7 @@ public HttpResponse doPreviewDescription(@QueryParameter String text) throws IOE
133130
translate(text, w);
134131
Map<String, String> extraHeaders = Collections.emptyMap();
135132
if (PREVIEWS_SET_CSP) {
136-
extraHeaders = Stream.of("Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy").collect(Collectors.toMap(Function.identity(), v -> "default-src 'none';"));
133+
extraHeaders = Map.of("Content-Security-Policy", "default-src 'none';");
137134
}
138135
return html(200, w.toString(), extraHeaders);
139136
}

core/src/main/java/hudson/model/DirectoryBrowserSupport.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import jenkins.security.MasterToSlaveCallable;
6565
import jenkins.security.ResourceDomainConfiguration;
6666
import jenkins.security.ResourceDomainRootAction;
67+
import jenkins.security.csp.CspHeader;
6768
import jenkins.util.SystemProperties;
6869
import jenkins.util.VirtualFile;
6970
import org.apache.commons.io.IOUtils;
@@ -398,13 +399,14 @@ private void serveFile(StaplerRequest2 req, StaplerResponse2 rsp, VirtualFile ro
398399
rsp.sendRedirect(302, ResourceDomainRootAction.get().getRedirectUrl(resourceToken, req.getRestOfPath()));
399400
} else {
400401
if (!ResourceDomainConfiguration.isResourceRequest(req)) {
401-
// if we're serving this from the main domain, set CSP headers
402+
// If we're serving this from the main domain, set CSP headers. These override the default CSP headers.
402403
String csp = SystemProperties.getString(CSP_PROPERTY_NAME, DEFAULT_CSP_VALUE);
403404
if (!csp.trim().isEmpty()) {
404405
// allow users to prevent sending this header by setting empty system property
405-
for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {
406-
rsp.setHeader(header, csp);
407-
}
406+
rsp.setHeader(CspHeader.ContentSecurityPolicy.getHeaderName(), csp);
407+
} else {
408+
// Clear the header value if configured by the user.
409+
rsp.setHeader(CspHeader.ContentSecurityPolicy.getHeaderName(), null);
408410
}
409411
}
410412
InputStream in;

core/src/main/java/hudson/model/UsageStatistics.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
import jenkins.security.FIPS140;
6868
import jenkins.util.SystemProperties;
6969
import net.sf.json.JSONObject;
70+
import org.kohsuke.accmod.Restricted;
71+
import org.kohsuke.accmod.restrictions.NoExternalUse;
7072
import org.kohsuke.stapler.StaplerRequest2;
7173

7274
/**
@@ -103,8 +105,7 @@ public UsageStatistics(String keyImage) {
103105
* Returns true if it's time for us to check for new version.
104106
*/
105107
public boolean isDue() {
106-
// user opted out (explicitly or FIPS is requested). no data collection
107-
if (!Jenkins.get().isUsageStatisticsCollected() || DISABLED || FIPS140.useCompliantAlgorithms()) {
108+
if (!isEnabled()) {
108109
return false;
109110
}
110111

@@ -116,6 +117,19 @@ public boolean isDue() {
116117
return false;
117118
}
118119

120+
/**
121+
* Returns whether between UI configuration, system property, and environment,
122+
* usage statistics should be submitted.
123+
*
124+
* @return true if and only if usage stats should be submitted
125+
* @since TODO
126+
*/
127+
@Restricted(NoExternalUse.class)
128+
public static boolean isEnabled() {
129+
// user opted out (explicitly or FIPS is requested). no data collection
130+
return Jenkins.get().isUsageStatisticsCollected() && !DISABLED && !FIPS140.useCompliantAlgorithms();
131+
}
132+
119133
private RSAPublicKey getKey() {
120134
try {
121135
if (key == null) {

core/src/main/java/hudson/util/FormFieldValidator.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,7 @@ private void _errorWithMarkup(String message, String cssClass) throws IOExceptio
231231
} else {
232232
response.setContentType("text/html;charset=UTF-8");
233233
if (APPLY_CONTENT_SECURITY_POLICY_HEADERS) {
234-
for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {
235-
response.setHeader(header, "sandbox; default-src 'none';");
236-
}
234+
response.setHeader("Content-Security-Policy", "sandbox; default-src 'none';");
237235
}
238236
response.getWriter().print("<div class=" + cssClass + ">" +
239237
message + "</div>");

core/src/main/java/hudson/util/FormValidation.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -612,9 +612,7 @@ public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object n
612612
protected void respond(StaplerResponse2 rsp, String html) throws IOException, ServletException {
613613
rsp.setContentType("text/html;charset=UTF-8");
614614
if (APPLY_CONTENT_SECURITY_POLICY_HEADERS) {
615-
for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {
616-
rsp.setHeader(header, "sandbox; default-src 'none';");
617-
}
615+
rsp.setHeader("Content-Security-Policy", "sandbox; default-src 'none';");
618616
}
619617
rsp.getWriter().print(html);
620618
}

core/src/main/java/jenkins/model/navigation/UserAction.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
@Extension(ordinal = -1)
4343
public class UserAction implements RootAction {
4444

45+
@Restricted(NoExternalUse.class)
46+
public static final String AVATAR_SIZE = "96x96";
47+
4548
@Override
4649
public String getIconFileName() {
4750
User current = User.current();
@@ -50,7 +53,7 @@ public String getIconFileName() {
5053
return null;
5154
}
5255

53-
return getAvatar(current, "96x96");
56+
return getAvatar(current, AVATAR_SIZE);
5457
}
5558

5659
@Override
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2025, CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package jenkins.security.csp;
26+
27+
import hudson.DescriptorExtensionList;
28+
import hudson.ExtensionList;
29+
import hudson.ExtensionPoint;
30+
import hudson.model.Describable;
31+
import java.util.Optional;
32+
import jenkins.model.Jenkins;
33+
import jenkins.security.csp.impl.CspConfiguration;
34+
import org.kohsuke.accmod.Restricted;
35+
import org.kohsuke.accmod.restrictions.Beta;
36+
37+
/**
38+
* Add more advanced options to the {@link jenkins.security.csp.impl.CspConfiguration} UI.
39+
*
40+
* @since TODO
41+
*/
42+
@Restricted(Beta.class)
43+
public abstract class AdvancedConfiguration implements Describable<AdvancedConfiguration>, ExtensionPoint {
44+
public static DescriptorExtensionList<AdvancedConfiguration, AdvancedConfigurationDescriptor> all() {
45+
return Jenkins.get().getDescriptorList(AdvancedConfiguration.class);
46+
}
47+
48+
/**
49+
* Return the currently configured {@link jenkins.security.csp.AdvancedConfiguration}, if any.
50+
*
51+
* @param clazz the {@link jenkins.security.csp.AdvancedConfiguration} type to look up
52+
* @param <T> the {@link jenkins.security.csp.AdvancedConfiguration} type to look up
53+
* @return the configured instance, if any
54+
*/
55+
public static <T extends AdvancedConfiguration> Optional<T> getCurrent(Class<T> clazz) {
56+
return ExtensionList.lookupSingleton(CspConfiguration.class).getAdvanced().stream()
57+
.filter(a -> a.getClass() == clazz)
58+
.map(clazz::cast)
59+
.findFirst();
60+
}
61+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2025, CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package jenkins.security.csp;
26+
27+
import hudson.model.Descriptor;
28+
import org.kohsuke.accmod.Restricted;
29+
import org.kohsuke.accmod.restrictions.Beta;
30+
31+
/**
32+
* Descriptor for {@link jenkins.security.csp.AdvancedConfiguration}.
33+
*
34+
* @since TODO
35+
*/
36+
@Restricted(Beta.class)
37+
public abstract class AdvancedConfigurationDescriptor extends Descriptor<AdvancedConfiguration> {
38+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2025, CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package jenkins.security.csp;
26+
27+
import edu.umd.cs.findbugs.annotations.CheckForNull;
28+
import hudson.Extension;
29+
import hudson.ExtensionList;
30+
import java.net.URI;
31+
import java.net.URISyntaxException;
32+
import java.util.Set;
33+
import java.util.concurrent.ConcurrentHashMap;
34+
import java.util.logging.Level;
35+
import java.util.logging.Logger;
36+
import org.kohsuke.accmod.Restricted;
37+
import org.kohsuke.accmod.restrictions.Beta;
38+
39+
/**
40+
* This is a general extension for use by implementations of {@link hudson.tasks.UserAvatarResolver}
41+
* and {@code AvatarMetadataAction} from {@code scm-api} plugin, or other "avatar-like" use cases.
42+
* It simplifies allowlisting safe sources of avatars by offering simple APIs that take a complete URL.
43+
*/
44+
@Restricted(Beta.class)
45+
@Extension
46+
public class AvatarContributor implements Contributor {
47+
private static final Logger LOGGER = Logger.getLogger(AvatarContributor.class.getName());
48+
49+
private final Set<String> domains = ConcurrentHashMap.newKeySet();
50+
51+
@Override
52+
public void apply(CspBuilder cspBuilder) {
53+
domains.forEach(d -> cspBuilder.add("img-src", d));
54+
}
55+
56+
/**
57+
* Request addition of the domain of the specified URL to the allowed set of avatar image domains.
58+
* <p>
59+
* This is a utility method intended to accept any avatar URL from an undetermined, but trusted (for images) domain.
60+
* If the specified URL is not {@code null}, has a host, and {@code http} or {@code https} scheme, its domain will
61+
* be added to the set of allowed domains.
62+
* </p>
63+
* <p>
64+
* <strong>Important:</strong> Only implementations restricting specification of avatar URLs to at least somewhat
65+
* privileged users to should invoke this method, for example users with at least {@link hudson.model.Item#CONFIGURE}
66+
* permission. Note that this guidance may change over time and require implementation changes.
67+
* </p>
68+
*
69+
* @param url The avatar image URL whose domain should be added to the list of allowed domains
70+
*/
71+
public static void allow(@CheckForNull String url) {
72+
String domain = extractDomainFromUrl(url);
73+
74+
if (domain == null) {
75+
LOGGER.log(Level.FINE, "Skipping null domain in avatar URL: " + url);
76+
return;
77+
}
78+
79+
if (ExtensionList.lookupSingleton(AvatarContributor.class).domains.add(domain)) {
80+
LOGGER.log(Level.CONFIG, "Adding domain '" + domain + "' from avatar URL: " + url);
81+
} else {
82+
LOGGER.log(Level.FINEST, "Skipped adding duplicate domain '" + domain + "' from avatar URL: " + url);
83+
}
84+
}
85+
86+
/**
87+
* Utility method extracting the domain specification for CSP fetch directives from a specified URL.
88+
* If the specified URL is not {@code null}, has a host, and {@code http} or {@code https} scheme, this method
89+
* will return its domain.
90+
* This can be used by implementations of {@link jenkins.security.csp.Contributor} for which {@link #allow(String)}
91+
* is not flexible enough (e.g., requesting administrator approval for a domain).
92+
*
93+
* @param url the URL
94+
* @return the domain from the specified URL, or {@code null} if the URL does not satisfy the stated conditions
95+
*/
96+
@CheckForNull
97+
public static String extractDomainFromUrl(@CheckForNull String url) {
98+
if (url == null) {
99+
return null;
100+
}
101+
try {
102+
final URI uri = new URI(url);
103+
final String host = uri.getHost();
104+
if (host == null) {
105+
// If there's no host, assume a local path
106+
LOGGER.log(Level.FINER, "Ignoring URI without host: " + url);
107+
return null;
108+
}
109+
String domain = host;
110+
final String scheme = uri.getScheme();
111+
if (scheme != null) {
112+
if (scheme.equals("http") || scheme.equals("https")) {
113+
domain = scheme + "://" + domain;
114+
} else {
115+
LOGGER.log(Level.FINER, "Ignoring URI with unsupported scheme: " + url);
116+
return null;
117+
}
118+
}
119+
final int port = uri.getPort();
120+
if (port != -1) {
121+
domain = domain + ":" + port;
122+
}
123+
return domain;
124+
} catch (URISyntaxException e) {
125+
LOGGER.log(Level.FINE, "Failed to parse avatar URI: " + url, e);
126+
return null;
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)