VYPR
Moderate severityNVD Advisory· Published Sep 23, 2020· Updated Aug 4, 2024

CVE-2020-2281

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.

PackageAffected versionsPatched versions
org.6wind.jenkins:lockable-resourcesMaven
< 2.92.9

Affected products

2

Patches

1
50ab82498f79

[SECURITY-1958]

https://github.com/jenkinsci/lockable-resources-pluginTobias GruetzmacherSep 18, 2020via ghsa
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

News mentions

1