CVE-2020-2281
Description
CSRF vulnerability in Jenkins Lockable Resources Plugin allows attackers to reserve, unreserve, unlock, and reset resources without user consent.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CSRF vulnerability in Jenkins Lockable Resources Plugin allows attackers to reserve, unreserve, unlock, and reset resources without user consent.
Vulnerability
Overview
The Jenkins Lockable Resources Plugin up to version 2.8 lacks cross-site request forgery (CSRF) protection on several action endpoints. The plugin allows managing lockable resources (e.g., printers, computers) via UI actions. However, the doUnlock, doReserve, doUnreserve, and doReset methods were not annotated with @RequirePOST, making them vulnerable to CSRF attacks [1][2].
Attack
Vector
An attacker can craft a malicious web page or email that, when visited by an authenticated Jenkins user, triggers a false HTTP request to perform unauthorized operations. The attacker does not need special permissions beyond tricking a user with appropriate permissions (e.g., RESERVE, UNLOCK) to make the request [3][4]. This leverages the user's active session to bypass authentication checks.
Impact
Successful exploitation allows an attacker to reserve, unreserve, unlock, or reset lockable resources. This can lead to denial of service (locking resources indefinitely) or interference with build processes relying on those resources. The attacker can manipulate resource availability, causing builds to wait or fail unexpectedly [3].
Mitigation
Jenkins Lockable Resources Plugin version 2.9 fixes the vulnerability by adding @RequirePOST annotations to the affected methods [2]. Users should upgrade to 2.9 or later. There is no known workaround; upgrading is the recommended action [4].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.6wind.jenkins:lockable-resourcesMaven | < 2.9 | 2.9 |
Affected products
2- Range: unspecified
Patches
150ab82498f79[SECURITY-1958]
5 files changed · +83 −5
src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java+5 −0 modified@@ -26,6 +26,7 @@ import org.jenkins.plugins.lockableresources.Messages; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.interceptor.RequirePOST; @Extension public class LockableResourcesRootAction implements RootAction { @@ -84,6 +85,7 @@ public int getNumberOfAllLabels() { return LockableResourcesManager.get().getAllLabels().size(); } + @RequirePOST public void doUnlock(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { Jenkins.get().checkPermission(UNLOCK); @@ -102,6 +104,7 @@ public void doUnlock(StaplerRequest req, StaplerResponse rsp) rsp.forwardToPreviousPage(req); } + @RequirePOST public void doReserve(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { Jenkins.get().checkPermission(RESERVE); @@ -122,6 +125,7 @@ public void doReserve(StaplerRequest req, StaplerResponse rsp) rsp.forwardToPreviousPage(req); } + @RequirePOST public void doUnreserve(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { Jenkins.get().checkPermission(RESERVE); @@ -146,6 +150,7 @@ public void doUnreserve(StaplerRequest req, StaplerResponse rsp) rsp.forwardToPreviousPage(req); } + @RequirePOST public void doReset(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { Jenkins.get().checkPermission(UNLOCK);
src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/index.jelly+8 −2 modified@@ -1,6 +1,6 @@ <!-- Copyright 2013, 6WIND S.A. All rights reserved. - Copyright 2019, TobiX + Copyright 2019-2020, TobiX This file is part of the Jenkins Lockable Resources Plugin and is published under the MIT license. @@ -16,7 +16,13 @@ return resourceName; } function resource_action(button, action) { - window.location.assign(action + "?resource=" + find_resource_name(button)); + // TODO: Migrate to form:link after Jenkins 2.233 (for button-styled links) + var form = document.createElement('form'); + form.setAttribute('method', 'POST'); + form.setAttribute('action', action + "?resource=" + find_resource_name(button)); + crumb.appendToForm(form); + document.body.appendChild(form); + form.submit(); } </script>
src/test/java/org/jenkins/plugins/lockableresources/LockableResourceApiTest.java+47 −0 added@@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: MIT + * Copyright (c) 2020, Tobias Gruetzmacher + */ +package org.jenkins.plugins.lockableresources; + +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.jenkins.plugins.lockableresources.TestHelpers.clickButton; +import static org.junit.Assert.assertThrows; + +public class LockableResourceApiTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void reserveUnreserveApi() throws Exception { + LockableResourcesManager.get().createResource("a1"); + + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + j.jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy()); + + JenkinsRule.WebClient wc = j.createWebClient(); + wc.login("user"); + clickButton(wc, "reserve"); + assertThat(LockableResourcesManager.get().fromName("a1").isReserved(), is(true)); + clickButton(wc, "unreserve"); + assertThat(LockableResourcesManager.get().fromName("a1").isReserved(), is(false)); + } + + @Test + @Issue("SECURITY-1958") + public void apiUsageHttpGet() { + JenkinsRule.WebClient wc = j.createWebClient(); + FailingHttpStatusCodeException e = assertThrows(FailingHttpStatusCodeException.class, + () -> wc.goTo("lockable-resources/reserve?resource=resource1")); + assertThat(e.getStatusCode(), is(405)); + } + +}
src/test/java/org/jenkins/plugins/lockableresources/LockStepTest.java+4 −3 modified@@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasEntry; +import static org.jenkins.plugins.lockableresources.TestHelpers.clickButton; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -434,7 +435,7 @@ public void unlockButtonWithWaitingRuns() throws Exception { j.waitForMessage( "[resource1] is locked by " + prevBuild.getFullDisplayName() + ", waiting...", rNext); isPaused(rNext, 1, 1); - wc.goTo("lockable-resources/unlock?resource=resource1"); + clickButton(wc, "unlock"); } j.waitForMessage("Lock acquired on [resource1]", rNext); @@ -494,7 +495,7 @@ public void manualUnreserveUnblocksJob() throws Exception { LockableResourcesManager.get().createResource("resource1"); JenkinsRule.WebClient wc = j.createWebClient(); - wc.goTo("lockable-resources/reserve?resource=resource1"); + clickButton(wc, "reserve"); LockableResource resource1 = LockableResourcesManager.get().fromName("resource1"); assertNotNull(resource1); resource1.setReservedBy("someone"); @@ -516,7 +517,7 @@ public void manualUnreserveUnblocksJob() throws Exception { WorkflowRun r = p.scheduleBuild2(0).waitForStart(); j.waitForMessage("[resource1] is locked, waiting...", r); - wc.goTo("lockable-resources/unreserve?resource=resource1"); + clickButton(wc, "unreserve"); SemaphoreStep.waitForStart("wait-inside/1", r); SemaphoreStep.success("wait-inside/1", null); j.assertBuildStatusSuccess(j.waitForCompletion(r));
src/test/java/org/jenkins/plugins/lockableresources/TestHelpers.java+19 −0 modified@@ -6,9 +6,13 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import com.gargoylesoftware.htmlunit.html.HtmlElement; +import com.gargoylesoftware.htmlunit.html.HtmlElementUtil; +import com.gargoylesoftware.htmlunit.html.HtmlPage; import hudson.model.FreeStyleProject; import hudson.model.Queue; import java.io.IOException; +import java.util.List; import jenkins.model.Jenkins; import net.sf.json.JSONArray; import net.sf.json.JSONObject; @@ -65,4 +69,19 @@ public static JSONObject getResourceFromApi( public static JSONObject getApiData(JenkinsRule rule) throws IOException { return rule.getJSON("plugin/lockable-resources/api/json").getJSONObject(); } + + // Currently assumes one resource or only clicks the button for the first resource + public static void clickButton(JenkinsRule.WebClient wc, String action) throws Exception { + HtmlPage htmlPage = wc.goTo("lockable-resources"); + List<HtmlElement> allButtons = htmlPage.getDocumentElement().getElementsByTagName("button"); + + HtmlElement reserveButton = null; + for (HtmlElement b : allButtons) { + String onClick = b.getAttribute("onClick"); + if (onClick != null && onClick.contains(action)) { + reserveButton = b; + } + } + HtmlElementUtil.click(reserveButton); + } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-rvww-w62m-hch8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-2281ghsaADVISORY
- www.openwall.com/lists/oss-security/2020/09/23/1ghsamailing-listx_refsource_MLISTWEB
- github.com/jenkinsci/lockable-resources-plugin/commit/50ab82498f792775a761e6f4937607b240ecde67ghsaWEB
- www.jenkins.io/security/advisory/2020-09-23/ghsax_refsource_CONFIRMWEB
News mentions
1- Jenkins Security Advisory 2020-09-23Jenkins Security Advisories · Sep 23, 2020