Jenkins does not perform a permission check in an HTTP endpoint
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
Details
Jenkins 2.470 and earlier, LTS 2.452.3 and earlier does not perform a permission check in an HTTP endpoint. This allows attackers with Overall/Read permission to access other users' "My Views". Attackers with global View/Configure and View/Delete permissions are also able to change other users' "My Views". Jenkins 2.471, LTS 2.452.4, LTS 2.462.1 restricts access to a user’s "My Views" to the owning user and administrators.
The fix
[SECURITY-3349]
core/src/main/java/hudson/model/MyViewsProperty.java+23 −2
@@ -26,6 +26,7 @@import edu.umd.cs.findbugs.annotations.CheckForNull;import edu.umd.cs.findbugs.annotations.NonNull;+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;import hudson.Extension;import hudson.Util;import hudson.model.Descriptor.FormException;@@ -41,6 +42,7 @@import java.util.concurrent.CopyOnWriteArrayList;import javax.servlet.ServletException;import jenkins.model.Jenkins;+import jenkins.util.SystemProperties;import net.sf.json.JSONObject;import org.jenkinsci.Symbol;import org.kohsuke.accmod.Restricted;@@ -50,6 +52,7 @@import org.kohsuke.stapler.HttpResponse;import org.kohsuke.stapler.QueryParameter;import org.kohsuke.stapler.StaplerFallback;+import org.kohsuke.stapler.StaplerProxy;import org.kohsuke.stapler.StaplerRequest;import org.kohsuke.stapler.StaplerResponse;import org.kohsuke.stapler.verb.POST;@@ -59,7 +62,14 @@** @author Tom Huybrechts*/-public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback {+public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback, StaplerProxy {++/**+* Escape hatch for StaplerProxy-based access control+*/+@Restricted(NoExternalUse.class)+@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console")+public static /* non-final */ boolean SKIP_PERMISSION_CHECK = SystemProperties.getBoolean(MyViewsProperty.class.getName() + ".skipPermissionCheck");/*** Name of the primary view defined by the user.@@ -225,7 +235,10 @@ public String getDisplayName() {@Overridepublic String getIconFileName() {-return "symbol-browsers";+if (SKIP_PERMISSION_CHECK || getACL().hasPermission(Jenkins.ADMINISTER))+return "symbol-browsers";+else+return null;}@Override@@ -233,6 +246,14 @@ public String getUrlName() {return "my-views";}+@Override+public Object getTarget() {+if (!SKIP_PERMISSION_CHECK) {+checkPermission(Jenkins.ADMINISTER);+}+return this;+}+@Extension @Symbol("myView")public static class DescriptorImpl extends UserPropertyDescriptor {
test/src/test/java/hudson/model/Security3349Test.java+80 −0
@@ -0,0 +1,80 @@+package hudson.model;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertFalse;+import static org.junit.Assert.assertTrue;++import jenkins.model.Jenkins;+import org.htmlunit.html.HtmlPage;+import org.junit.Rule;+import org.junit.Test;+import org.jvnet.hudson.test.FlagRule;+import org.jvnet.hudson.test.Issue;+import org.jvnet.hudson.test.JenkinsRule;+import org.jvnet.hudson.test.MockAuthorizationStrategy;++public class Security3349Test {++@Rule+public JenkinsRule rule = new JenkinsRule();++@Rule public FlagRule<Boolean> skipPermissionCheck = new FlagRule<>(() -> MyViewsProperty.SKIP_PERMISSION_CHECK, x -> MyViewsProperty.SKIP_PERMISSION_CHECK = x);++@Test+@Issue("SECURITY-3349")+public void usersCannotAccessOtherUsersViews() throws Exception {+User user = User.getOrCreateByIdOrFullName("user");+User admin = User.getOrCreateByIdOrFullName("admin");++rule.jenkins.setSecurityRealm(rule.createDummySecurityRealm());+MockAuthorizationStrategy mockAuthorizationStrategy = new MockAuthorizationStrategy();+mockAuthorizationStrategy.grant(Jenkins.READ, View.READ).everywhere().to("user");+mockAuthorizationStrategy.grant(Jenkins.ADMINISTER).everywhere().to("admin");+rule.jenkins.setAuthorizationStrategy(mockAuthorizationStrategy);++MyViewsProperty prop1 = new MyViewsProperty(null);+MyView usersView = new MyView("User's view", prop1);+user.addProperty(prop1);+prop1.setUser(user);+prop1.addView(usersView);++MyViewsProperty prop2 = new MyViewsProperty(null);+MyView adminsView = new MyView("Admin's view", prop2);+admin.addProperty(prop2);+prop2.setUser(admin);+prop2.addView(adminsView);++try (JenkinsRule.WebClient wc = rule.createWebClient()) {+wc.setThrowExceptionOnFailingStatusCode(false);+wc.login("user");++HtmlPage adminViews = wc.goTo("user/admin/my-views/view/all/");+assertEquals(403, adminViews.getWebResponse().getStatusCode());++HtmlPage adminUserPage = wc.goTo("user/admin/");+assertFalse(adminUserPage.getWebResponse().getContentAsString().contains("My Views"));++HtmlPage userViews = wc.goTo("user/user/my-views/view/all/");+assertEquals(200, userViews.getWebResponse().getStatusCode());++HtmlPage userUserPage = wc.goTo("user/user/");+assertTrue(userUserPage.getWebResponse().getContentAsString().contains("My Views"));++wc.login("admin");++adminViews = wc.goTo("user/admin/my-views/view/all/");+assertEquals(200, adminViews.getWebResponse().getStatusCode());+userViews = wc.goTo("user/user/my-views/view/all/");+assertEquals(200, userViews.getWebResponse().getStatusCode());++MyViewsProperty.SKIP_PERMISSION_CHECK = true;++wc.login("user");+adminViews = wc.goTo("user/admin/my-views/view/all/");+assertEquals(200, adminViews.getWebResponse().getStatusCode());+adminUserPage = wc.goTo("user/admin/");+assertTrue(adminUserPage.getWebResponse().getContentAsString().contains("My Views"));++}+}+}
[SECURITY-3349]
core/src/main/java/hudson/model/MyViewsProperty.java+23 −2
@@ -26,6 +26,7 @@import edu.umd.cs.findbugs.annotations.CheckForNull;import edu.umd.cs.findbugs.annotations.NonNull;+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;import hudson.Extension;import hudson.Util;import hudson.model.Descriptor.FormException;@@ -42,6 +43,7 @@import java.util.concurrent.CopyOnWriteArrayList;import javax.servlet.ServletException;import jenkins.model.Jenkins;+import jenkins.util.SystemProperties;import net.sf.json.JSONObject;import org.jenkinsci.Symbol;import org.kohsuke.accmod.Restricted;@@ -51,6 +53,7 @@import org.kohsuke.stapler.HttpResponse;import org.kohsuke.stapler.QueryParameter;import org.kohsuke.stapler.StaplerFallback;+import org.kohsuke.stapler.StaplerProxy;import org.kohsuke.stapler.StaplerRequest;import org.kohsuke.stapler.StaplerResponse;import org.kohsuke.stapler.verb.POST;@@ -60,7 +63,14 @@** @author Tom Huybrechts*/-public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback {+public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback, StaplerProxy {++/**+* Escape hatch for StaplerProxy-based access control+*/+@Restricted(NoExternalUse.class)+@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console")+public static /* non-final */ boolean SKIP_PERMISSION_CHECK = SystemProperties.getBoolean(MyViewsProperty.class.getName() + ".skipPermissionCheck");/*** Name of the primary view defined by the user.@@ -226,7 +236,10 @@ public String getDisplayName() {@Overridepublic String getIconFileName() {-return "symbol-browsers";+if (SKIP_PERMISSION_CHECK || getACL().hasPermission(Jenkins.ADMINISTER))+return "symbol-browsers";+else+return null;}@Override@@ -234,6 +247,14 @@ public String getUrlName() {return "my-views";}+@Override+public Object getTarget() {+if (!SKIP_PERMISSION_CHECK) {+checkPermission(Jenkins.ADMINISTER);+}+return this;+}+@Extension @Symbol("myView")public static class DescriptorImpl extends UserPropertyDescriptor {
test/src/test/java/hudson/model/Security3349Test.java+80 −0
@@ -0,0 +1,80 @@+package hudson.model;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertFalse;+import static org.junit.Assert.assertTrue;++import jenkins.model.Jenkins;+import org.htmlunit.html.HtmlPage;+import org.junit.Rule;+import org.junit.Test;+import org.jvnet.hudson.test.FlagRule;+import org.jvnet.hudson.test.Issue;+import org.jvnet.hudson.test.JenkinsRule;+import org.jvnet.hudson.test.MockAuthorizationStrategy;++public class Security3349Test {++@Rule+public JenkinsRule rule = new JenkinsRule();++@Rule public FlagRule<Boolean> skipPermissionCheck = new FlagRule<>(() -> MyViewsProperty.SKIP_PERMISSION_CHECK, x -> MyViewsProperty.SKIP_PERMISSION_CHECK = x);++@Test+@Issue("SECURITY-3349")+public void usersCannotAccessOtherUsersViews() throws Exception {+User user = User.getOrCreateByIdOrFullName("user");+User admin = User.getOrCreateByIdOrFullName("admin");++rule.jenkins.setSecurityRealm(rule.createDummySecurityRealm());+MockAuthorizationStrategy mockAuthorizationStrategy = new MockAuthorizationStrategy();+mockAuthorizationStrategy.grant(Jenkins.READ, View.READ).everywhere().to("user");+mockAuthorizationStrategy.grant(Jenkins.ADMINISTER).everywhere().to("admin");+rule.jenkins.setAuthorizationStrategy(mockAuthorizationStrategy);++MyViewsProperty prop1 = new MyViewsProperty(null);+MyView usersView = new MyView("User's view", prop1);+user.addProperty(prop1);+prop1.setUser(user);+prop1.addView(usersView);++MyViewsProperty prop2 = new MyViewsProperty(null);+MyView adminsView = new MyView("Admin's view", prop2);+admin.addProperty(prop2);+prop2.setUser(admin);+prop2.addView(adminsView);++try (JenkinsRule.WebClient wc = rule.createWebClient()) {+wc.setThrowExceptionOnFailingStatusCode(false);+wc.login("user");++HtmlPage adminViews = wc.goTo("user/admin/my-views/view/all/");+assertEquals(403, adminViews.getWebResponse().getStatusCode());++HtmlPage adminUserPage = wc.goTo("user/admin/");+assertFalse(adminUserPage.getWebResponse().getContentAsString().contains("My Views"));++HtmlPage userViews = wc.goTo("user/user/my-views/view/all/");+assertEquals(200, userViews.getWebResponse().getStatusCode());++HtmlPage userUserPage = wc.goTo("user/user/");+assertTrue(userUserPage.getWebResponse().getContentAsString().contains("My Views"));++wc.login("admin");++adminViews = wc.goTo("user/admin/my-views/view/all/");+assertEquals(200, adminViews.getWebResponse().getStatusCode());+userViews = wc.goTo("user/user/my-views/view/all/");+assertEquals(200, userViews.getWebResponse().getStatusCode());++MyViewsProperty.SKIP_PERMISSION_CHECK = true;++wc.login("user");+adminViews = wc.goTo("user/admin/my-views/view/all/");+assertEquals(200, adminViews.getWebResponse().getStatusCode());+adminUserPage = wc.goTo("user/admin/");+assertTrue(adminUserPage.getWebResponse().getContentAsString().contains("My Views"));++}+}+}
[SECURITY-3349]
core/src/main/java/hudson/model/MyViewsProperty.java+23 −2
@@ -26,6 +26,7 @@import edu.umd.cs.findbugs.annotations.CheckForNull;import edu.umd.cs.findbugs.annotations.NonNull;+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;import hudson.Extension;import hudson.Util;import hudson.model.Descriptor.FormException;@@ -41,6 +42,7 @@import java.util.concurrent.CopyOnWriteArrayList;import javax.servlet.ServletException;import jenkins.model.Jenkins;+import jenkins.util.SystemProperties;import net.sf.json.JSONObject;import org.jenkinsci.Symbol;import org.kohsuke.accmod.Restricted;@@ -50,6 +52,7 @@import org.kohsuke.stapler.HttpResponse;import org.kohsuke.stapler.QueryParameter;import org.kohsuke.stapler.StaplerFallback;+import org.kohsuke.stapler.StaplerProxy;import org.kohsuke.stapler.StaplerRequest;import org.kohsuke.stapler.StaplerResponse;import org.kohsuke.stapler.verb.POST;@@ -59,7 +62,14 @@** @author Tom Huybrechts*/-public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback {+public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback, StaplerProxy {++/**+* Escape hatch for StaplerProxy-based access control+*/+@Restricted(NoExternalUse.class)+@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console")+public static /* non-final */ boolean SKIP_PERMISSION_CHECK = SystemProperties.getBoolean(MyViewsProperty.class.getName() + ".skipPermissionCheck");/*** Name of the primary view defined by the user.@@ -225,7 +235,10 @@ public String getDisplayName() {@Overridepublic String getIconFileName() {-return "symbol-browsers";+if (SKIP_PERMISSION_CHECK || getACL().hasPermission(Jenkins.ADMINISTER))+return "symbol-browsers";+else+return null;}@Override@@ -233,6 +246,14 @@ public String getUrlName() {return "my-views";}+@Override+public Object getTarget() {+if (!SKIP_PERMISSION_CHECK) {+checkPermission(Jenkins.ADMINISTER);+}+return this;+}+@Extension @Symbol("myView")public static class DescriptorImpl extends UserPropertyDescriptor {
test/src/test/java/hudson/model/Security3349Test.java+80 −0
@@ -0,0 +1,80 @@+package hudson.model;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertFalse;+import static org.junit.Assert.assertTrue;++import jenkins.model.Jenkins;+import org.htmlunit.html.HtmlPage;+import org.junit.Rule;+import org.junit.Test;+import org.jvnet.hudson.test.FlagRule;+import org.jvnet.hudson.test.Issue;+import org.jvnet.hudson.test.JenkinsRule;+import org.jvnet.hudson.test.MockAuthorizationStrategy;++public class Security3349Test {++@Rule+public JenkinsRule rule = new JenkinsRule();++@Rule public FlagRule<Boolean> skipPermissionCheck = new FlagRule<>(() -> MyViewsProperty.SKIP_PERMISSION_CHECK, x -> MyViewsProperty.SKIP_PERMISSION_CHECK = x);++@Test+@Issue("SECURITY-3349")+public void usersCannotAccessOtherUsersViews() throws Exception {+User user = User.getOrCreateByIdOrFullName("user");+User admin = User.getOrCreateByIdOrFullName("admin");++rule.jenkins.setSecurityRealm(rule.createDummySecurityRealm());+MockAuthorizationStrategy mockAuthorizationStrategy = new MockAuthorizationStrategy();+mockAuthorizationStrategy.grant(Jenkins.READ, View.READ).everywhere().to("user");+mockAuthorizationStrategy.grant(Jenkins.ADMINISTER).everywhere().to("admin");+rule.jenkins.setAuthorizationStrategy(mockAuthorizationStrategy);++MyViewsProperty prop1 = new MyViewsProperty(null);+MyView usersView = new MyView("User's view", prop1);+user.addProperty(prop1);+prop1.setUser(user);+prop1.addView(usersView);++MyViewsProperty prop2 = new MyViewsProperty(null);+MyView adminsView = new MyView("Admin's view", prop2);+admin.addProperty(prop2);+prop2.setUser(admin);+prop2.addView(adminsView);++try (JenkinsRule.WebClient wc = rule.createWebClient()) {+wc.setThrowExceptionOnFailingStatusCode(false);+wc.login("user");++HtmlPage adminViews = wc.goTo("user/admin/my-views/view/all/");+assertEquals(403, adminViews.getWebResponse().getStatusCode());++HtmlPage adminUserPage = wc.goTo("user/admin/");+assertFalse(adminUserPage.getWebResponse().getContentAsString().contains("My Views"));++HtmlPage userViews = wc.goTo("user/user/my-views/view/all/");+assertEquals(200, userViews.getWebResponse().getStatusCode());++HtmlPage userUserPage = wc.goTo("user/user/");+assertTrue(userUserPage.getWebResponse().getContentAsString().contains("My Views"));++wc.login("admin");++adminViews = wc.goTo("user/admin/my-views/view/all/");+assertEquals(200, adminViews.getWebResponse().getStatusCode());+userViews = wc.goTo("user/user/my-views/view/all/");+assertEquals(200, userViews.getWebResponse().getStatusCode());++MyViewsProperty.SKIP_PERMISSION_CHECK = true;++wc.login("user");+adminViews = wc.goTo("user/admin/my-views/view/all/");+assertEquals(200, adminViews.getWebResponse().getStatusCode());+adminUserPage = wc.goTo("user/admin/");+assertTrue(adminUserPage.getWebResponse().getContentAsString().contains("My Views"));++}+}+}
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2024-43045
- WEBhttps://github.com/jenkinsci/jenkins/commit/0c13259cebc6a780fee7825838f4dd98ece8e68a
- WEBhttps://github.com/jenkinsci/jenkins/commit/3752f406bfef764e4954238acf44343169ae5799
- WEBhttps://github.com/jenkinsci/jenkins/commit/efece77d759b38c95b39b191051a8203bbc2f428
- PACKAGEhttps://github.com/jenkinsci/jenkins
- WEBhttps://www.jenkins.io/security/advisory/2024-08-07/#SECURITY-3349