VYPR
Moderate severityNVD Advisory· Published Apr 21, 2021· Updated Aug 3, 2024

CVE-2021-21644

CVE-2021-21644

Description

A cross-site request forgery (CSRF) vulnerability in Jenkins Config File Provider Plugin 3.7.0 and earlier allows attackers to delete configuration files corresponding to an attacker-specified ID.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A CSRF vulnerability in Jenkins Config File Provider Plugin up to 3.7.0 allows attackers to delete arbitrary configuration files by tricking an authenticated user into visiting a malicious link.

Vulnerability

Jenkins Config File Provider Plugin versions 3.7.0 and earlier contain a cross-site request forgery (CSRF) vulnerability in an HTTP endpoint that does not require POST requests [1][2]. This endpoint allows deleting configuration files by specifying an ID. The vulnerability exists because the fix for SECURITY-938 was incomplete [2].

Exploitation

An attacker can exploit this vulnerability by crafting a malicious link or page that triggers a GET request to the vulnerable endpoint while the victim is authenticated to Jenkins. The attacker must know or guess the ID of a configuration file to delete; no special permissions on the target file are required [2]. User interaction (e.g., clicking a link) is necessary [2].

Impact

Successful exploitation allows an attacker to delete arbitrary configuration files in Jenkins, which can disrupt builds, jobs, or other functionality that depends on those files [2]. This is a medium-severity issue with CVSS score not specified; only the advisory indicates it is a CSRF leading to deletion of configuration files [2].

Mitigation

Upgrade to Config File Provider Plugin version 3.7.1 or later, which was released on 2021-04-21 [2][4]. This version adds the missing CSRF protection by requiring POST requests for the endpoint [2]. No known workarounds aside from the upgrade; the plugin is not EOL [3].

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.jenkins-ci.plugins:config-file-providerMaven
< 3.7.13.7.1

Affected products

2

Patches

1
9ffc32379477

SECURITY-2203

7 files changed · +230 17
  • src/main/java/org/jenkinsci/plugins/configfiles/buildwrapper/ManagedFile.java+8 1 modified
    @@ -106,7 +106,10 @@ public String getDisplayName() {
                 return "";
             }
     
    -        public ListBoxModel doFillFileIdItems(@AncestorInPath ItemGroup context) {
    +        public ListBoxModel doFillFileIdItems(@AncestorInPath ItemGroup context, @AncestorInPath Item project) {
    +            // You should have permission to configure your project in order to get the available managed files
    +            project.checkPermission(Item.CONFIGURE);
    +            
                 ListBoxModel items = new ListBoxModel();
                 items.add("please select", "");
                 for (Config config : ConfigFiles.getConfigsInContext(context, null)) {
    @@ -124,6 +127,10 @@ public ListBoxModel doFillFileIdItems(@AncestorInPath ItemGroup context) {
              * @return a validation result / description
              */
             public HttpResponse doCheckFileId(StaplerRequest req, @AncestorInPath Item context, @QueryParameter String fileId) {
    +            // You should have permission to configure your project in order to check whether the selected file id is
    +            // allowed to you
    +            context.checkPermission(Item.CONFIGURE);
    +            
                 final Config config = ConfigFiles.getByIdOrNull(context, fileId);
                 if (config != null) {
                     return ConfigFileDetailLinkDescription.getDescription(req, context, fileId);
    
  • src/main/java/org/jenkinsci/plugins/configfiles/ConfigFilesManagement.java+18 13 modified
    @@ -67,6 +67,16 @@ public ConfigFilesManagement() {
             this.store = GlobalConfigFiles.get();
         }
     
    +    /**
    +     * The global configuration actions are exclusive of Jenkins administer.
    +     * @return The target.
    +     */
    +    @Override
    +    public Object getTarget() {
    +        checkPermission(Jenkins.ADMINISTER);
    +        return this;
    +    }
    +    
         /**
          * @see hudson.model.Action#getDisplayName()
          */
    @@ -139,7 +149,7 @@ public Collection<Config> getConfigs() {
          */
         @POST
         public HttpResponse doSaveConfig(StaplerRequest req) {
    -        checkPermission(Jenkins.ADMINISTER);
    +        // permission handled in getTarget
             try {
                 JSONObject json = req.getSubmittedForm().getJSONObject("config");
                 Config config = req.bindJSON(Config.class, json);
    @@ -158,7 +168,7 @@ public HttpResponse doSaveConfig(StaplerRequest req) {
         }
     
         public void doShow(StaplerRequest req, StaplerResponse rsp, @QueryParameter("id") String configId) throws IOException, ServletException {
    -        checkPermission(Jenkins.ADMINISTER);
    +        // permission handled in getTarget
     
             Config config = store.getById(configId);
             req.setAttribute("contentType", config.getProvider().getContentType());
    @@ -176,7 +186,7 @@ public void doShow(StaplerRequest req, StaplerResponse rsp, @QueryParameter("id"
          * @throws ServletException
          */
         public void doEditConfig(StaplerRequest req, StaplerResponse rsp, @QueryParameter("id") String configId) throws IOException, ServletException {
    -        checkPermission(Jenkins.ADMINISTER);
    +        // permission handled in getTarget
     
             Config config = store.getById(configId);
             req.setAttribute("contentType", config.getProvider().getContentType());
    @@ -196,7 +206,7 @@ public void doEditConfig(StaplerRequest req, StaplerResponse rsp, @QueryParamete
          */
         @POST
         public void doAddConfig(StaplerRequest req, StaplerResponse rsp, @QueryParameter("providerId") String providerId, @QueryParameter("configId") String configId) throws IOException, ServletException {
    -        checkPermission(Jenkins.ADMINISTER);
    +        // permission handled in getTarget
     
             FormValidation error = null;
             if (providerId == null || providerId.isEmpty()) {
    @@ -238,7 +248,7 @@ public void doAddConfig(StaplerRequest req, StaplerResponse rsp, @QueryParameter
         }
     
         public void doSelectProvider(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    -        checkPermission(Jenkins.ADMINISTER);
    +        // permission handled in getTarget
             req.setAttribute("providers", ConfigProvider.all());
             req.setAttribute("configId", UUID.randomUUID().toString());
             req.getView(this, JELLY_RESOURCES_PATH + "selectprovider.jelly").forward(req, rsp);
    @@ -259,14 +269,16 @@ private void checkPermission(Permission permission) {
          */
         @RequirePOST
         public HttpResponse doRemoveConfig(StaplerRequest res, StaplerResponse rsp, @QueryParameter("id") String configId) throws IOException {
    -        checkPermission(Jenkins.ADMINISTER);
    +        // permission handled in getTarget
     
             store.remove(configId);
     
             return new HttpRedirect("index");
         }
     
         public FormValidation doCheckConfigId(@QueryParameter("configId") String configId) {
    +        // permission handled in getTarget
    +
             if (configId == null || configId.isEmpty()) {
                 return FormValidation.warning(Messages.ConfigFilesManagement_configIdCannotBeEmpty());
             }
    @@ -282,11 +294,4 @@ public FormValidation doCheckConfigId(@QueryParameter("configId") String configI
                 return FormValidation.warning(Messages.ConfigFilesManagement_configIdAlreadyUsed(config.name, config.id));
             }
         }
    -
    -
    -    @Override
    -    public Object getTarget() {
    -        checkPermission(Item.EXTENDED_READ);
    -        return this;
    -    }
     }
    
  • src/main/java/org/jenkinsci/plugins/configfiles/folder/FolderConfigFileAction.java+2 0 modified
    @@ -238,6 +238,8 @@ public HttpResponse doRemoveConfig(StaplerRequest res, StaplerResponse rsp, @Que
     
         @Override
         public FormValidation doCheckConfigId(@QueryParameter("configId") String configId) {
    +        checkPermission(Job.CONFIGURE);
    +        
             if (configId == null || configId.isEmpty()) {
                 return FormValidation.warning(Messages.ConfigFilesManagement_configIdCannotBeEmpty());
             }
    
  • src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnGlobalSettingsProvider.java+4 1 modified
    @@ -2,6 +2,7 @@
     
     import hudson.Extension;
     import hudson.FilePath;
    +import hudson.model.Item;
     import hudson.model.ItemGroup;
     import hudson.model.TaskListener;
     import hudson.slaves.WorkspaceList;
    @@ -130,7 +131,9 @@ public String getDisplayName() {
                 return "provided global settings.xml";
             }
     
    -        public ListBoxModel doFillSettingsConfigIdItems(@AncestorInPath ItemGroup context) {
    +        public ListBoxModel doFillSettingsConfigIdItems(@AncestorInPath ItemGroup context, @AncestorInPath Item project) {
    +            project.checkPermission(Item.CONFIGURE);
    +            
                 ListBoxModel items = new ListBoxModel();
                 items.add("please select", "");
                 for (Config config : ConfigFiles.getConfigsInContext(context, GlobalMavenSettingsConfigProvider.class)) {
    
  • src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnSettingsProvider.java+4 2 modified
    @@ -6,9 +6,9 @@
     import java.util.logging.Level;
     import java.util.logging.Logger;
     
    +import hudson.model.Item;
     import org.apache.commons.lang.StringUtils;
     import org.jenkinsci.lib.configprovider.model.Config;
    -import org.jenkinsci.lib.configprovider.model.ConfigFileManager;
     import org.jenkinsci.plugins.configfiles.ConfigFiles;
     import org.jenkinsci.plugins.configfiles.common.CleanTempFilesAction;
     import org.jenkinsci.plugins.configfiles.maven.MavenSettingsConfig;
    @@ -133,7 +133,9 @@ public String getDisplayName() {
                 return Messages.MvnSettingsProvider_ProvidedSettings();
             }
     
    -        public ListBoxModel doFillSettingsConfigIdItems(@AncestorInPath ItemGroup context) {
    +        public ListBoxModel doFillSettingsConfigIdItems(@AncestorInPath ItemGroup context, @AncestorInPath Item project) {
    +            project.checkPermission(Item.CONFIGURE);
    +            
                 ListBoxModel items = new ListBoxModel();
                 items.add(Messages.MvnSettingsProvider_PleaseSelect(), "");
                 for (Config config : ConfigFiles.getConfigsInContext(context, MavenSettingsConfigProvider.class)) {
    
  • src/test/java/org/jenkinsci/plugins/configfiles/folder/FolderConfigFileActionTest.java+39 0 modified
    @@ -3,6 +3,9 @@
     import com.cloudbees.hudson.plugins.folder.Folder;
     import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
     import com.gargoylesoftware.htmlunit.html.HtmlPage;
    +import hudson.model.Item;
    +import jenkins.model.Jenkins;
    +import org.hamcrest.MatcherAssert;
     import org.hamcrest.Matchers;
     import org.jenkinsci.lib.configprovider.ConfigProvider;
     import org.jenkinsci.lib.configprovider.model.Config;
    @@ -19,6 +22,7 @@
     import org.jvnet.hudson.test.BuildWatcher;
     import org.jvnet.hudson.test.Issue;
     import org.jvnet.hudson.test.JenkinsRule;
    +import org.jvnet.hudson.test.MockAuthorizationStrategy;
     
     import java.io.IOException;
     import java.util.Collection;
    @@ -27,6 +31,7 @@
     import java.util.concurrent.atomic.AtomicReference;
     
     import static org.hamcrest.Matchers.is;
    +import static org.hamcrest.Matchers.notNullValue;
     import static org.hamcrest.collection.IsEmptyCollection.empty;
     import static org.junit.Assert.assertEquals;
     import static org.junit.Assert.assertNotEquals;
    @@ -250,6 +255,40 @@ public void xssPreventionInFolder() throws Exception {
             assertThat(store.getConfigs(), empty());
         }
     
    +    @Test
    +    @Issue("SECURITY-2203")
    +    public void folderCheckConfigIdProtected() throws Exception {
    +        // ----------
    +        // Create a new folder
    +        Folder f1 = createFolder();
    +        f1.save();
    +
    +        // ----------
    +        // let's allow all people to see the folder, but not configure it
    +        r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
    +        r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
    +                // read access to everyone
    +                .grant(Jenkins.READ).everywhere().toEveryone()
    +                .grant(Item.DISCOVER).everywhere().toAuthenticated()
    +                .grant(Item.READ).onItems(f1).toEveryone()
    +                        
    +                // config access on the folder to this user
    +                .grant(Item.CONFIGURE).onFolders(f1).to("folderConfigurer")
    +        );
    +
    +        // ----------
    +        // An user without permission cannot see the form to add a new config file
    +        JenkinsRule.WebClient wc = r.createWebClient();
    +        wc.login("reader");
    +        wc.assertFails(f1.getUrl() +  "configfiles/selectProvider", 404);
    +
    +        // ----------
    +        // The person with permission can access
    +        wc.login("folderConfigurer");
    +        HtmlPage page = wc.goTo(f1.getUrl() +  "configfiles/selectProvider");
    +        MatcherAssert.assertThat(page, notNullValue());
    +    }
    +    
         private CpsFlowDefinition getNewJobDefinition() {
             return new CpsFlowDefinition("" +
                     "node {\n" +
    
  • src/test/java/org/jenkinsci/plugins/configfiles/Security2203Test.java+155 0 added
    @@ -0,0 +1,155 @@
    +package org.jenkinsci.plugins.configfiles;
    +
    +import hudson.model.FreeStyleProject;
    +import hudson.model.Item;
    +import hudson.model.ItemGroup;
    +import hudson.model.User;
    +import hudson.security.ACL;
    +import hudson.security.ACLContext;
    +import hudson.security.AccessDeniedException2;
    +import hudson.security.Permission;
    +import jenkins.model.Jenkins;
    +import org.jenkinsci.plugins.configfiles.buildwrapper.ManagedFile;
    +import org.jenkinsci.plugins.configfiles.maven.job.MvnGlobalSettingsProvider;
    +import org.jenkinsci.plugins.configfiles.maven.job.MvnSettingsProvider;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.jvnet.hudson.test.Issue;
    +import org.jvnet.hudson.test.JenkinsRule;
    +import org.jvnet.hudson.test.MockAuthorizationStrategy;
    +import org.kohsuke.stapler.StaplerRequest;
    +
    +import java.io.IOException;
    +import java.util.AbstractMap;
    +import java.util.Map;
    +import java.util.stream.Collectors;
    +import java.util.stream.Stream;
    +
    +import static org.hamcrest.MatcherAssert.assertThat;
    +import static org.hamcrest.Matchers.equalTo;
    +import static org.junit.Assert.fail;
    +
    +/**
    + * Testing there is no information disclosure.
    + */
    +public class Security2203Test {
    +    @Rule
    +    public JenkinsRule r = new JenkinsRule();
    +
    +    private FreeStyleProject project;
    +
    +    @Before
    +    public void setUpAuthorizationAndProject() throws IOException {
    +        project = r.jenkins.createProject(FreeStyleProject.class, "j");
    +
    +        r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
    +        r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().
    +                grant(Jenkins.READ, Item.READ).everywhere().to("reader").
    +                grant(Item.CONFIGURE).onItems(project).to("projectConfigurer").
    +                grant(Jenkins.ADMINISTER).everywhere().to("administer")
    +        );
    +    }
    +
    +    /**
    +     * The {@link ManagedFile.DescriptorImpl#doFillFileIdItems(ItemGroup, Item)} is only accessible by people able to
    +     * configure the job.
    +     */
    +    @Issue("SECURITY-2203")
    +    @Test
    +    public void managedFileDoFillFiledIdItemsProtected() {
    +        Runnable run = () -> {
    +            ManagedFile.DescriptorImpl descriptor = (ManagedFile.DescriptorImpl) Jenkins.get().getDescriptorOrDie(ManagedFile.class);
    +            descriptor.doFillFileIdItems(Jenkins.get(), project);
    +        };
    +
    +        assertWhoCanExecute(run,Item.CONFIGURE, "ManagedFile.DescriptorImpl#doFillFileIdItems");
    +    }
    +
    +    /**
    +     * The {@link ManagedFile.DescriptorImpl#doCheckFileId(StaplerRequest, Item, String)} is only accessible by people
    +     * able to configure the job.
    +     */
    +    @Issue("SECURITY-2203")
    +    @Test
    +    public void managedFileDoCheckFileIdProtected() {
    +        Runnable run = () -> {
    +            ManagedFile.DescriptorImpl descriptor = (ManagedFile.DescriptorImpl) Jenkins.get().getDescriptorOrDie(ManagedFile.class);
    +            descriptor.doCheckFileId(null, project, "fileId"); // request won't be used, we can use null
    +        };
    +
    +        assertWhoCanExecute(run, Item.CONFIGURE, "ManagedFile.DescriptorImpl#doCheckFileId");
    +    }
    +    
    +    /**
    +     * The {@link MvnGlobalSettingsProvider.DescriptorImpl#doFillSettingsConfigIdItems(ItemGroup, Item)} is only accessible by people able to
    +     * configure the job.
    +     */
    +    @Issue("SECURITY-2203")
    +    @Test
    +    public void mvnGlobalSettingsProviderDoFillSettingsConfigIdItemsProtected() {
    +        Runnable run = () -> {
    +            MvnGlobalSettingsProvider.DescriptorImpl descriptor = (MvnGlobalSettingsProvider.DescriptorImpl) Jenkins.get().getDescriptorOrDie(MvnGlobalSettingsProvider.class);
    +            descriptor.doFillSettingsConfigIdItems(Jenkins.get(), project);
    +        };
    +        
    +        assertWhoCanExecute(run, Item.CONFIGURE, "MvnGlobalSettingsProvider.DescriptorImpl#doFillSettingsConfigIdItems");
    +    }
    +
    +    /**
    +     * The {@link MvnSettingsProvider.DescriptorImpl#doFillSettingsConfigIdItems(ItemGroup, Item)} is only accessible by people able to
    +     * configure the job.
    +     */
    +    @Issue("SECURITY-2203")
    +    @Test
    +    public void mvnSettingsProviderDoFillSettingsConfigIdItemsProtected() {
    +        Runnable run = () -> {
    +            MvnSettingsProvider.DescriptorImpl descriptor = (MvnSettingsProvider.DescriptorImpl) Jenkins.get().getDescriptorOrDie(MvnSettingsProvider.class);
    +            descriptor.doFillSettingsConfigIdItems(Jenkins.get(), project);
    +        };
    +
    +        assertWhoCanExecute(run, Item.CONFIGURE, "MvnSettingsProvider.DescriptorImpl#doFillSettingsConfigIdItems");
    +    }
    +
    +    /**
    +     * The {@link ConfigFilesManagement#getTarget()} is only accessible by people able to administer jenkins. It guarantees
    +     * all methods in the class require {@link Jenkins#ADMINISTER}.
    +     */
    +    @Issue("SECURITY-2203")
    +    @Test
    +    public void configFilesManagementAllMethodsProtected() {
    +        Runnable run = () -> {
    +            ConfigFilesManagement configFilesManagement = Jenkins.get().getExtensionList(ConfigFilesManagement.class).get(0);
    +            configFilesManagement.getTarget();
    +        };
    +
    +        assertWhoCanExecute(run, Jenkins.ADMINISTER, "ConfigFilesManagement#getTarget");
    +    }
    +    
    +    /**
    +     * Common logic to check a specific method is accessible by people with Configure permission and not by any person 
    +     * with just read permission. We don't care about the result. If you don't have permission, the method with fail.
    +     * @param run The method to check.
    +     * @param checkedMethod The name of the method for logging purposes.
    +     */
    +    private void assertWhoCanExecute(Runnable run, Permission permission, String checkedMethod) {
    +        final Map<Permission, String> userWithPermission = Stream.of(
    +                new AbstractMap.SimpleEntry<>(Jenkins.READ, "reader"),
    +                new AbstractMap.SimpleEntry<>(Item.CONFIGURE, "projectConfigurer"),
    +                new AbstractMap.SimpleEntry<>(Jenkins.ADMINISTER, "administer"))
    +                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    +
    +        try (ACLContext ctx = ACL.as(User.getOrCreateByIdOrFullName("reader"))) {
    +            run.run(); // The method should fail
    +            fail(String.format("%s should be only accessible by people with the permission %s, but it's accessible by a person with %s", checkedMethod, permission, Item.READ));
    +        } catch (AccessDeniedException2 e) {
    +            assertThat(e.permission, equalTo(permission));
    +        }
    +
    +        try (ACLContext ctx = ACL.as(User.getOrCreateByIdOrFullName(userWithPermission.get(permission)))) {
    +            run.run(); // The method doesn't fail
    +        } catch (AccessDeniedException2 e) {
    +            fail(String.format("%s should be accessible to people with the permission %s but it failed with the exception: %s", checkedMethod, permission, e));
    +        }
    +    }
    +}
    

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