Security context
Medium· 4.3GHSA-p3f5-98cv-562j CVE-2025-67636CWE-862Published Dec 10, 2025

Jenkins is missing a permission check on password fields

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

2.529 → fixed in 2.5410 → fixed in 2.528.3

Details

A missing permission check in Jenkins 2.540 and earlier, LTS 2.528.2 and earlier allows attackers with View/Read permission to view encrypted password values in views.

The fix

[SECURITY-1809]

Kevin-CB· Dec 2, 2025, 01:46 PM+30573ee7380c5e
core/src/main/java/hudson/Functions.java+38 7
@@ -203,6 +203,12 @@ public class Functions {
private static final AtomicLong iota = new AtomicLong();
private static Logger LOGGER = Logger.getLogger(Functions.class.getName());
+ /**
+ * Escape hatch to use the non-recursive f:password masking.
+ */
+ private static /* non-final */ boolean NON_RECURSIVE_PASSWORD_MASKING_PERMISSION_CHECK = SystemProperties.getBoolean(Functions.class.getName() + ".nonRecursivePasswordMaskingPermissionCheck");
+
+
public Functions() {
}
@@ -2252,13 +2258,38 @@ public String getPasswordValue(Object o) {
StaplerRequest2 req = Stapler.getCurrentRequest2();
if (o instanceof Secret || Secret.BLANK_NONSECRET_PASSWORD_FIELDS_WITHOUT_ITEM_CONFIGURE) {
if (req != null) {
- Item item = req.findAncestorObject(Item.class);
- if (item != null && !item.hasPermission(Item.CONFIGURE)) {
- return "********";
- }
- Computer computer = req.findAncestorObject(Computer.class);
- if (computer != null && !computer.hasPermission(Computer.CONFIGURE)) {
- return "********";
+ if (NON_RECURSIVE_PASSWORD_MASKING_PERMISSION_CHECK) {
+ Item item = req.findAncestorObject(Item.class);
+ if (item != null && !item.hasPermission(Item.CONFIGURE)) {
+ return "********";
+ }
+ Computer computer = req.findAncestorObject(Computer.class);
+ if (computer != null && !computer.hasPermission(Computer.CONFIGURE)) {
+ return "********";
+ }
+ } else {
+ List<Ancestor> ancestors = req.getAncestors();
+ for (Ancestor ancestor : Iterators.reverse(ancestors)) {
+ Object type = ancestor.getObject();
+ if (type instanceof Item item) {
+ if (!item.hasPermission(Item.CONFIGURE)) {
+ return "********";
+ }
+ break;
+ }
+ if (type instanceof Computer computer) {
+ if (!computer.hasPermission(Computer.CONFIGURE)) {
+ return "********";
+ }
+ break;
+ }
+ if (type instanceof View view) {
+ if (!view.hasPermission(View.CONFIGURE)) {
+ return "********";
+ }
+ break;
+ }
+ }
}
}
}
test/src/test/java/jenkins/security/Security1809Test.java+245 0
@@ -0,0 +1,245 @@
+package jenkins.security;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+
+import com.cloudbees.hudson.plugins.folder.Folder;
+import hudson.model.Action;
+import hudson.model.Computer;
+import hudson.model.FreeStyleBuild;
+import hudson.model.FreeStyleProject;
+import hudson.model.Item;
+import hudson.model.ListView;
+import hudson.model.View;
+import hudson.slaves.DumbSlave;
+import hudson.util.Secret;
+import jenkins.model.Jenkins;
+import org.htmlunit.Page;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.MockAuthorizationStrategy;
+import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
+
+@WithJenkins
+class Security1809Test {
+
+ private JenkinsRule j;
+
+ private final String password = "p4ssw0rd";
+
+ private final Secret secretPassword = Secret.fromString(password);
+
+ @BeforeEach
+ void setUp(JenkinsRule rule) {
+ j = rule;
+ }
+
+ @Test
+ @Issue("SECURITY-1809")
+ void passwordIsMaskedForView() throws Exception {
+ final PasswordView view = new PasswordView("view1", secretPassword);
+ j.jenkins.addView(view);
+
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
+ .grant(Jenkins.READ, View.READ).everywhere().to("readUser")
+ .grant(Jenkins.READ, View.READ, View.CONFIGURE).everywhere().to("configureUser"));
+
+ String url = view.getUrl() + "password";
+
+ // configure permission allow to see encrypted value
+ assertContainsOnlyEncryptedSecret("configureUser", url);
+
+ // read permission get only redacted value
+ assertContainsOnlyMaskedSecret("readUser", url);
+ }
+
+ @Test
+ @Issue("SECURITY-1809")
+ void passwordIsMaskedForPrimaryView() throws Exception {
+ final PasswordView view = new PasswordView("view1", secretPassword);
+ j.jenkins.addView(view);
+ j.jenkins.setPrimaryView(view);
+
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
+ .grant(Jenkins.READ).everywhere().to("readUser")
+ .grant(Jenkins.READ, View.READ, View.CONFIGURE).everywhere().to("configureUser"));
+
+ String url = "password";
+
+ // configure permission allow to see encrypted value
+ assertContainsOnlyEncryptedSecret("configureUser", url);
+
+ // read permission get only redacted value
+ assertContainsOnlyMaskedSecret("readUser", url);
+ }
+
+ @Test
+ void passwordIsMaskedForAgent() throws Exception {
+ final DumbSlave agent = j.createSlave("agent1", "", null);
+
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
+ .grant(Jenkins.READ, Computer.CONFIGURE).everywhere().to("configureUser")
+ .grant(Jenkins.READ).everywhere().to("readUser"));
+
+ agent.toComputer().addAction(new PasswordAction(secretPassword));
+
+ String url = agent.toComputer().getUrl() + "password";
+
+ // configure permission allow to see encrypted value
+ assertContainsOnlyEncryptedSecret("configureUser", url);
+
+ // read permission get only redacted value
+ assertContainsOnlyMaskedSecret("readUser", url);
+ }
+
+ @Test
+ void passwordIsMaskedForJob() throws Exception {
+ final FreeStyleProject job = j.createFreeStyleProject();
+ FreeStyleBuild build = j.buildAndAssertSuccess(job);
+ build.addAction(new PasswordAction(secretPassword));
+
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
+ .grant(Jenkins.READ, Item.READ, Item.CONFIGURE).everywhere().to("configureUser")
+ .grant(Jenkins.READ, Item.READ).everywhere().to("readUser"));
+
+ String url = build.getUrl() + "password";
+
+ // configure permission allow to see encrypted value
+ assertContainsOnlyEncryptedSecret("configureUser", url);
+
+ // read permission get only redacted value
+ assertContainsOnlyMaskedSecret("readUser", url);
+ }
+
+ @Test
+ void permissionIsCheckedOnClosestAncestor() throws Exception {
+ final PasswordView view = new PasswordView("view1", secretPassword);
+ j.jenkins.addView(view);
+
+ final FreeStyleProject job = j.createFreeStyleProject("job1");
+ FreeStyleBuild build = j.buildAndAssertSuccess(job);
+ build.addAction(new ActionWithView(view));
+
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
+ .grant(Jenkins.READ, Item.READ, View.READ, Item.CONFIGURE).everywhere().to("itemConfigureUser")
+ .grant(Jenkins.READ, Item.READ, View.READ, View.CONFIGURE).everywhere().to("viewConfigureUser"));
+
+ String url = build.getUrl() + "myAction/view/password";
+
+ // View/Configure permission allow to see encrypted value
+ assertContainsOnlyEncryptedSecret("viewConfigureUser", url);
+
+ // Item/Configure permission get only redacted value
+ assertContainsOnlyMaskedSecret("itemConfigureUser", url);
+ }
+
+ @Test
+ @Issue("SECURITY-1809")
+ void permissionIsCorrectlyCheckedOnNestedObject() throws Exception {
+ final Folder folder = j.jenkins.createProject(Folder.class, "folder1");
+ final FreeStyleProject job = folder.createProject(FreeStyleProject.class, "job1");
+ FreeStyleBuild build = j.buildAndAssertSuccess(job);
+ build.addAction(new PasswordAction(secretPassword));
+
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
+ // Item.CONFIGURE on job1 but NOT on folder1
+ .grant(Jenkins.READ, Item.READ).everywhere().to("jobConfigureUser")
+ .grant(Item.CONFIGURE).onItems(job).to("jobConfigureUser")
+ // Item.CONFIGURE on folder1 but NOT on job1
+ .grant(Jenkins.READ, Item.READ).everywhere().to("folderConfigureUser")
+ .grant(Item.CONFIGURE).onItems(folder).to("folderConfigureUser"));
+
+ String url = build.getUrl() + "password";
+
+ // Item/Configure permission on job1 allow to see encrypted value
+ assertContainsOnlyEncryptedSecret("jobConfigureUser", url);
+
+ // Item/Configure permission only on folder1 get only redacted value
+ assertContainsOnlyMaskedSecret("folderConfigureUser", url);
+ }
+
+ private void assertContainsOnlyEncryptedSecret(String user, String url) throws Exception {
+ try (JenkinsRule.WebClient wc = j.createWebClient().login(user)) {
+ Page page = wc.goTo(url);
+ String content = page.getWebResponse().getContentAsString();
+
+ assertThat(content, not(containsString(password)));
+ assertThat(content, containsString(secretPassword.getEncryptedValue()));
+ }
+ }
+
+ private void assertContainsOnlyMaskedSecret(String user, String url) throws Exception {
+ try (JenkinsRule.WebClient wc = j.createWebClient().login(user)) {
+ Page page = wc.goTo(url);
+ String content = page.getWebResponse().getContentAsString();
+
+ assertThat(content, containsString("********"));
+ assertThat(content, not(containsString(password)));
+ assertThat(content, not(containsString(secretPassword.getEncryptedValue())));
+ }
+ }
+
+ public static class PasswordView extends ListView {
+ private final Secret secret;
+
+ PasswordView(String name, Secret secret) {
+ super(name);
+ this.secret = secret;
+ }
+
+ public Secret getSecret() {
+ return secret;
+ }
+ }
+
+ public static class PasswordAction implements Action {
+ private final Secret secret;
+
+ PasswordAction(Secret secret) {
+ this.secret = secret;
+ }
+
+ public Secret getSecret() {
+ return secret;
+ }
+
+ @Override
+ public String getIconFileName() { return null; }
+
+ @Override
+ public String getDisplayName() { return null; }
+
+ @Override
+ public String getUrlName() { return "password"; }
+ }
+
+ public static class ActionWithView implements Action {
+ private final PasswordView view;
+
+ ActionWithView(PasswordView view) {
+ this.view = view;
+ }
+
+ public PasswordView getView() {
+ return view;
+ }
+
+ @Override
+ public String getIconFileName() { return null; }
+
+ @Override
+ public String getDisplayName() { return null; }
+
+ @Override
+ public String getUrlName() { return "myAction"; }
+ }
+}
test/src/test/resources/jenkins/security/Security1809Test/PasswordAction/index.jelly+11 0
@@ -0,0 +1,11 @@
+<?jelly escape-by-default='true'?>
+<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
+ <l:layout title="Test Password">
+ <l:main-panel>
+ <j:set var="instance" value="${it}" />
+ <f:entry title="Password" field="secret">
+ <f:password />
+ </f:entry>
+ </l:main-panel>
+ </l:layout>
+</j:jelly>
test/src/test/resources/jenkins/security/Security1809Test/PasswordView/password.jelly+11 0
@@ -0,0 +1,11 @@
+<?jelly escape-by-default='true'?>
+<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
+ <l:layout title="Test Password - View">
+ <l:main-panel>
+ <j:set var="instance" value="${it}" />
+ <f:entry title="Password" field="secret">
+ <f:password />
+ </f:entry>
+ </l:main-panel>
+ </l:layout>
+</j:jelly>

References