Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
065c007
[JENKINS-76263] Add Content-Security-Policy header
daniel-beck Nov 5, 2025
2a9c11b
Remove obsolete headers from everywhere
daniel-beck Nov 5, 2025
a95436d
Make this configurable, and more
daniel-beck Nov 10, 2025
0acd2fc
Fix system property to set header name on UI
daniel-beck Nov 10, 2025
72cae10
Address PR feedback, fix form validation related to RRURL
daniel-beck Nov 11, 2025
d9653d7
Add UI tests
daniel-beck Nov 13, 2025
a2a544b
Merge commit '8f1bf5be69203c8d45d9ec4acbb0a7ef35ab8830' into JENKINS-…
daniel-beck Nov 13, 2025
4ed38f4
Various minor improvements and changes
daniel-beck Nov 13, 2025
ecd7424
Fix assertions
daniel-beck Nov 14, 2025
1877ed6
Specify `base-uri 'none'` by default, add support for non-fetch direc…
daniel-beck Nov 14, 2025
33f12b9
Minor adjustments
daniel-beck Nov 14, 2025
9d5b567
Add test for ReportingAction
daniel-beck Nov 14, 2025
b890166
Fix Spotless
daniel-beck Nov 14, 2025
c68bb5e
Add test coverage for CspBuilder#NONE_DIRECTIVES
daniel-beck Nov 14, 2025
4c8564f
Permission checks and require POST
daniel-beck Nov 14, 2025
3f4f8ed
Change button label
daniel-beck Nov 14, 2025
b2b7927
Also set CSP for filters implementing custom responses
daniel-beck Nov 15, 2025
698b9ab
Make loggers private
daniel-beck Nov 17, 2025
b8c9e77
Add test coverage for detecting changed CSP header
daniel-beck Nov 17, 2025
d571801
Add `AvatarContributor` utility class for use by plugins
daniel-beck Nov 21, 2025
5f1418c
Fix misplaced log message
daniel-beck Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions core/src/main/java/hudson/markup/MarkupFormatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,8 @@
import java.io.Writer;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jenkins.util.SystemProperties;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
Expand Down Expand Up @@ -133,7 +130,7 @@ public HttpResponse doPreviewDescription(@QueryParameter String text) throws IOE
translate(text, w);
Map<String, String> extraHeaders = Collections.emptyMap();
if (PREVIEWS_SET_CSP) {
extraHeaders = Stream.of("Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy").collect(Collectors.toMap(Function.identity(), v -> "default-src 'none';"));
extraHeaders = Map.of("Content-Security-Policy", "default-src 'none';");
}
return html(200, w.toString(), extraHeaders);
}
Expand Down
10 changes: 6 additions & 4 deletions core/src/main/java/hudson/model/DirectoryBrowserSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import jenkins.security.MasterToSlaveCallable;
import jenkins.security.ResourceDomainConfiguration;
import jenkins.security.ResourceDomainRootAction;
import jenkins.security.csp.CspHeader;
import jenkins.util.SystemProperties;
import jenkins.util.VirtualFile;
import org.apache.commons.io.IOUtils;
Expand Down Expand Up @@ -398,13 +399,14 @@ private void serveFile(StaplerRequest2 req, StaplerResponse2 rsp, VirtualFile ro
rsp.sendRedirect(302, ResourceDomainRootAction.get().getRedirectUrl(resourceToken, req.getRestOfPath()));
} else {
if (!ResourceDomainConfiguration.isResourceRequest(req)) {
// if we're serving this from the main domain, set CSP headers
// If we're serving this from the main domain, set CSP headers. These override the default CSP headers.
String csp = SystemProperties.getString(CSP_PROPERTY_NAME, DEFAULT_CSP_VALUE);
if (!csp.trim().isEmpty()) {
// allow users to prevent sending this header by setting empty system property
for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {
rsp.setHeader(header, csp);
}
rsp.setHeader(CspHeader.ContentSecurityPolicy.getHeaderName(), csp);
} else {
// Clear the header value if configured by the user.
rsp.setHeader(CspHeader.ContentSecurityPolicy.getHeaderName(), null);
}
}
InputStream in;
Expand Down
18 changes: 16 additions & 2 deletions core/src/main/java/hudson/model/UsageStatistics.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
import jenkins.security.FIPS140;
import jenkins.util.SystemProperties;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerRequest2;

/**
Expand Down Expand Up @@ -103,8 +105,7 @@
* Returns true if it's time for us to check for new version.
*/
public boolean isDue() {
// user opted out (explicitly or FIPS is requested). no data collection
if (!Jenkins.get().isUsageStatisticsCollected() || DISABLED || FIPS140.useCompliantAlgorithms()) {
if (!isEnabled()) {
return false;
}

Expand All @@ -116,6 +117,19 @@
return false;
}

/**
* Returns whether between UI configuration, system property, and environment,
* usage statistics should be submitted.
*
* @return true if and only if usage stats should be submitted
* @since TODO
*/
@Restricted(NoExternalUse.class)
public static boolean isEnabled() {
// user opted out (explicitly or FIPS is requested). no data collection
return Jenkins.get().isUsageStatisticsCollected() && !DISABLED && !FIPS140.useCompliantAlgorithms();

Check warning on line 130 in core/src/main/java/hudson/model/UsageStatistics.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 130 is only partially covered, one branch is missing
}

private RSAPublicKey getKey() {
try {
if (key == null) {
Expand Down
4 changes: 1 addition & 3 deletions core/src/main/java/hudson/util/FormFieldValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,7 @@
} else {
response.setContentType("text/html;charset=UTF-8");
if (APPLY_CONTENT_SECURITY_POLICY_HEADERS) {
for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {
response.setHeader(header, "sandbox; default-src 'none';");
}
response.setHeader("Content-Security-Policy", "sandbox; default-src 'none';");

Check warning on line 234 in core/src/main/java/hudson/util/FormFieldValidator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 234 is not covered by tests
}
response.getWriter().print("<div class=" + cssClass + ">" +
message + "</div>");
Expand Down
4 changes: 1 addition & 3 deletions core/src/main/java/hudson/util/FormValidation.java
Original file line number Diff line number Diff line change
Expand Up @@ -612,9 +612,7 @@ public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object n
protected void respond(StaplerResponse2 rsp, String html) throws IOException, ServletException {
rsp.setContentType("text/html;charset=UTF-8");
if (APPLY_CONTENT_SECURITY_POLICY_HEADERS) {
for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {
rsp.setHeader(header, "sandbox; default-src 'none';");
}
rsp.setHeader("Content-Security-Policy", "sandbox; default-src 'none';");
}
rsp.getWriter().print(html);
}
Expand Down
5 changes: 4 additions & 1 deletion core/src/main/java/jenkins/model/navigation/UserAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
@Extension(ordinal = -1)
public class UserAction implements RootAction {

@Restricted(NoExternalUse.class)
public static final String AVATAR_SIZE = "96x96";

@Override
public String getIconFileName() {
User current = User.current();
Expand All @@ -50,7 +53,7 @@ public String getIconFileName() {
return null;
}

return getAvatar(current, "96x96");
return getAvatar(current, AVATAR_SIZE);
}

@Override
Expand Down
61 changes: 61 additions & 0 deletions core/src/main/java/jenkins/security/csp/AdvancedConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.security.csp;

import hudson.DescriptorExtensionList;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Describable;
import java.util.Optional;
import jenkins.model.Jenkins;
import jenkins.security.csp.impl.CspConfiguration;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;

/**
* Add more advanced options to the {@link jenkins.security.csp.impl.CspConfiguration} UI.
*
* @since TODO
*/
@Restricted(Beta.class)
public abstract class AdvancedConfiguration implements Describable<AdvancedConfiguration>, ExtensionPoint {

Check warning on line 43 in core/src/main/java/jenkins/security/csp/AdvancedConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 43 is not covered by tests
public static DescriptorExtensionList<AdvancedConfiguration, AdvancedConfigurationDescriptor> all() {
return Jenkins.get().getDescriptorList(AdvancedConfiguration.class);
}

/**
* Return the currently configured {@link jenkins.security.csp.AdvancedConfiguration}, if any.
*
* @param clazz the {@link jenkins.security.csp.AdvancedConfiguration} type to look up
* @param <T> the {@link jenkins.security.csp.AdvancedConfiguration} type to look up
* @return the configured instance, if any
*/
public static <T extends AdvancedConfiguration> Optional<T> getCurrent(Class<T> clazz) {
return ExtensionList.lookupSingleton(CspConfiguration.class).getAdvanced().stream()
.filter(a -> a.getClass() == clazz)
.map(clazz::cast)
.findFirst();

Check warning on line 59 in core/src/main/java/jenkins/security/csp/AdvancedConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 56-59 are not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.security.csp;

import hudson.model.Descriptor;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;

/**
* Descriptor for {@link jenkins.security.csp.AdvancedConfiguration}.
*
* @since TODO
*/
@Restricted(Beta.class)
public abstract class AdvancedConfigurationDescriptor extends Descriptor<AdvancedConfiguration> {

Check warning on line 37 in core/src/main/java/jenkins/security/csp/AdvancedConfigurationDescriptor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 37 is not covered by tests
}
129 changes: 129 additions & 0 deletions core/src/main/java/jenkins/security/csp/AvatarContributor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.security.csp;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.Extension;
import hudson.ExtensionList;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;

/**
* This is a general extension for use by implementations of {@link hudson.tasks.UserAvatarResolver}
* and {@code AvatarMetadataAction} from {@code scm-api} plugin, or other "avatar-like" use cases.
* It simplifies allowlisting safe sources of avatars by offering simple APIs that take a complete URL.
*/
@Restricted(Beta.class)
@Extension
public class AvatarContributor implements Contributor {
private static final Logger LOGGER = Logger.getLogger(AvatarContributor.class.getName());

private final Set<String> domains = ConcurrentHashMap.newKeySet();

@Override
public void apply(CspBuilder cspBuilder) {
domains.forEach(d -> cspBuilder.add("img-src", d));
}

/**
* Request addition of the domain of the specified URL to the allowed set of avatar image domains.
* <p>
* This is a utility method intended to accept any avatar URL from an undetermined, but trusted (for images) domain.
* If the specified URL is not {@code null}, has a host, and {@code http} or {@code https} scheme, its domain will
* be added to the set of allowed domains.
* </p>
* <p>
* <strong>Important:</strong> Only implementations restricting specification of avatar URLs to at least somewhat
* privileged users to should invoke this method, for example users with at least {@link hudson.model.Item#CONFIGURE}
* permission. Note that this guidance may change over time and require implementation changes.
* </p>
*
* @param url The avatar image URL whose domain should be added to the list of allowed domains
*/
public static void allow(@CheckForNull String url) {
String domain = extractDomainFromUrl(url);

if (domain == null) {
LOGGER.log(Level.FINE, "Skipping null domain in avatar URL: " + url);
return;
}

if (ExtensionList.lookupSingleton(AvatarContributor.class).domains.add(domain)) {
LOGGER.log(Level.CONFIG, "Adding domain '" + domain + "' from avatar URL: " + url);
} else {
LOGGER.log(Level.FINEST, "Skipped adding duplicate domain '" + domain + "' from avatar URL: " + url);
}
}

/**
* Utility method extracting the domain specification for CSP fetch directives from a specified URL.
* If the specified URL is not {@code null}, has a host, and {@code http} or {@code https} scheme, this method
* will return its domain.
* This can be used by implementations of {@link jenkins.security.csp.Contributor} for which {@link #allow(String)}
* is not flexible enough (e.g., requesting administrator approval for a domain).
*
* @param url the URL
* @return the domain from the specified URL, or {@code null} if the URL does not satisfy the stated conditions
*/
@CheckForNull
public static String extractDomainFromUrl(@CheckForNull String url) {
if (url == null) {
return null;
}
try {
final URI uri = new URI(url);
final String host = uri.getHost();
if (host == null) {
// If there's no host, assume a local path
LOGGER.log(Level.FINER, "Ignoring URI without host: " + url);
return null;
}
String domain = host;
final String scheme = uri.getScheme();
if (scheme != null) {

Check warning on line 111 in core/src/main/java/jenkins/security/csp/AvatarContributor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 111 is only partially covered, one branch is missing
if (scheme.equals("http") || scheme.equals("https")) {
domain = scheme + "://" + domain;
} else {
LOGGER.log(Level.FINER, "Ignoring URI with unsupported scheme: " + url);
return null;
}
}
final int port = uri.getPort();
if (port != -1) {
domain = domain + ":" + port;
}
return domain;
} catch (URISyntaxException e) {
LOGGER.log(Level.FINE, "Failed to parse avatar URI: " + url, e);
return null;
}
}
}
Loading
Loading