VYPR
Moderate severityNVD Advisory· Published Aug 16, 2023· Updated Oct 8, 2024

CVE-2023-40339

CVE-2023-40339

Description

Jenkins Config File Provider Plugin 952.va_544a_6234b_46 and earlier does not mask (i.e., replace with asterisks) credentials specified in configuration files when they're written to the build log.

AI Insight

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

Jenkins Config File Provider Plugin prior to 953.v0432a_802e4d2 does not mask credentials in build logs, exposing sensitive values.

Vulnerability

Description Jenkins Config File Provider Plugin 952.va_544a_6234b_46 and earlier fails to mask (replace with asterisks) credentials specified in configuration files when they are written to the build log [1][3]. This means that any credential values stored in configuration files provided by the plugin appear in plaintext in the build console output.

Exploitation

An attacker who can view build logs can obtain these credentials directly from the log output. No additional privileges are required beyond the ability to access Jenkins build logs, which may be available to users with read access to jobs or even publicly if builds are exposed [2]. The vulnerability is triggered automatically whenever a build uses a configuration file containing credentials.

Impact

Successful exploitation leads to disclosure of sensitive credentials, such as passwords, tokens, or SSH keys, which can then be used to gain unauthorized access to other systems or services.

Mitigation

Jenkins has released version 953.v0432a_802e4d2 of the Config File Provider Plugin that masks credentials properly [2][4]. All users should upgrade to this version immediately. No workarounds are known.

AI Insight generated on May 20, 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
< 953.v0432a953.v0432a

Affected products

2

Patches

1
0432a802e4d2

SECURITY-3090

16 files changed · +646 96
  • pom.xml+4 0 modified
    @@ -85,6 +85,10 @@
                 <groupId>org.jenkins-ci.plugins</groupId>
                 <artifactId>ssh-credentials</artifactId>
             </dependency>
    +        <dependency>
    +            <groupId>org.jenkins-ci.plugins</groupId>
    +            <artifactId>credentials-binding</artifactId>
    +        </dependency>
             <dependency>
                 <groupId>org.jenkins-ci.plugins</groupId>
                 <artifactId>script-security</artifactId>
    
  • src/main/java/org/jenkinsci/lib/configprovider/ConfigProvider.java+21 0 modified
    @@ -26,6 +26,7 @@ of this software and associated documentation files (the "Software"), to deal
     import java.io.IOException;
     import java.lang.reflect.Field;
     import java.util.Collection;
    +import java.util.Collections;
     import java.util.List;
     
     import org.jenkinsci.lib.configprovider.model.Config;
    @@ -44,6 +45,7 @@ of this software and associated documentation files (the "Software"), to deal
     import hudson.model.TaskListener;
     import hudson.util.ReflectionUtils;
     import jenkins.model.Jenkins;
    +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
     
     /**
      * A ConfigProvider represents a configuration file (such as Maven's settings.xml) where the user can choose its actual content among several {@linkplain Config concrete contents} that are
    @@ -201,18 +203,37 @@ public void save(Config config) {
         /**
          * Provide the given content file.
          *
    +     * <strong>Implementation Note:</strong>If this is overridden in a sub class and credentials are injected into
    +     * the content - then the implementation must also override {@link #getSensitiveContentForMasking(Config, Run)} to
    +     * avoid accidental disclosure.
    +     *
          * @param configFile the file content to be provided
          * @param workDir target workspace directory
          * @param listener the listener
          * @param tempFiles temp files created by this method, these files will
          *                  be deleted by the caller
          * @return file content
          * @throws IOException in case an exception occurs when providing the content or other needed files
    +     * @see #getSensitiveContentForMasking(Config, Run)
          * @since 2.16
          */
         @CheckForNull
         public String supplyContent(@NonNull Config configFile, Run<?, ?> build, FilePath workDir, TaskListener listener, @NonNull List<String> tempFiles) throws IOException {
             return configFile.content;
         }
     
    +    /**
    +     * Obtain a list of sensitive Strings to mask for the given provider and build.
    +     * For example if a {@link UsernamePasswordCredentials}  is being
    +     * injected into a file then the password (and possibly the username) from the resolved credential
    +     * would need to be masked and should be returned here.
    +     *
    +     * @param configFile the file content to provide sensitive strings for.
    +     * @param build the build for which the configFile applies.
    +     * @return List of Strings that need to be masked in the console.
    +     */
    +    public @NonNull List<String> getSensitiveContentForMasking(Config configFile, Run<?, ?> build) {
    +        return Collections.emptyList();
    +    }
    +
     }
    
  • src/main/java/org/jenkinsci/plugins/configfiles/buildwrapper/ConfigFileBuildWrapper.java+52 0 modified
    @@ -2,6 +2,7 @@
      The MIT License
     
      Copyright (c) 2011, Dominik Bartholdi
    + Copyright (c) 2023, CloudBees Inc.
     
      Permission is hereby granted, free of charge, to any person obtaining a copy
      of this software and associated documentation files (the "Software"), to deal
    @@ -23,28 +24,41 @@ of this software and associated documentation files (the "Software"), to deal
      */
     package org.jenkinsci.plugins.configfiles.buildwrapper;
     
    +import edu.umd.cs.findbugs.annotations.NonNull;
    +
     import hudson.EnvVars;
     import hudson.Extension;
     import hudson.FilePath;
     import hudson.Launcher;
    +import hudson.console.ConsoleLogFilter;
     import hudson.model.AbstractProject;
     import hudson.model.Run;
     import hudson.model.TaskListener;
     import hudson.tasks.BuildWrapperDescriptor;
    +import hudson.util.Secret;
     
     import java.io.IOException;
    +import java.io.OutputStream;
    +import java.io.Serializable;
    +import java.nio.charset.Charset;
     import java.util.ArrayList;
    +import java.util.Collection;
     import java.util.List;
     import java.util.Map;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    +import java.util.regex.Pattern;
     
     import jenkins.tasks.SimpleBuildWrapper;
     
     import org.apache.commons.lang.StringUtils;
     import org.jenkinsci.Symbol;
     import org.kohsuke.stapler.DataBoundConstructor;
     
    +import org.jenkinsci.lib.configprovider.model.Config;
    +import org.jenkinsci.plugins.configfiles.ConfigFiles;
    +import org.jenkinsci.plugins.credentialsbinding.masking.SecretPatterns;
    +
     public class ConfigFileBuildWrapper extends SimpleBuildWrapper {
     
         private List<ManagedFile> managedFiles = new ArrayList<ManagedFile>();
    @@ -75,6 +89,25 @@ public void setUp(Context context, Run<?, ?> build, FilePath workspace, Launcher
             }
         }
     
    +    private synchronized List<String> getSecretValuesToMask(Run<?,?> build) {
    +        List<String> seecretsToMask = new ArrayList<>();
    +        for (ManagedFile managedFile : managedFiles) {
    +            Config config = ConfigFiles.getByIdOrNull(build, managedFile.getFileId());
    +            seecretsToMask.addAll(config.getProvider().getSensitiveContentForMasking(config, build));
    +        }
    +        return seecretsToMask;
    +    }
    +
    +    @Override
    +    public ConsoleLogFilter createLoggerDecorator(@NonNull Run<?, ?> build) {
    +        List<String> secretValues = getSecretValuesToMask(build);
    +        if (secretValues.isEmpty()) {
    +            // no secrets so no filtering
    +            return null;
    +        }
    +        return new SecretFilter(secretValues, build.getCharset());
    +    }
    +
         public List<ManagedFile> getManagedFiles() {
             return managedFiles;
         }
    @@ -115,4 +148,23 @@ public void tearDown(Run<?, ?> build, FilePath workspace, Launcher launcher, Tas
     
         }
     
    +    private static final class SecretFilter extends ConsoleLogFilter implements Serializable {
    +
    +        private static final long serialVersionUID = 1;
    +
    +        private Secret pattern;
    +        private String charset;
    +
    +        SecretFilter(Collection<String> secrets, Charset cs) {
    +            pattern = Secret.fromString(SecretPatterns.getAggregateSecretPattern(secrets).pattern());
    +            charset = cs.name();
    +        }
    +
    +        @Override
    +        public OutputStream decorateLogger(Run build, OutputStream logger) {
    +            return new SecretPatterns.MaskingOutputStream(logger, () -> Pattern.compile(pattern.getPlainText()), charset);
    +        }
    +
    +    }
    +
     }
    
  • src/main/java/org/jenkinsci/plugins/configfiles/custom/AbstractCustomProvider.java+7 0 modified
    @@ -24,6 +24,7 @@ of this software and associated documentation files (the "Software"), to deal
     package org.jenkinsci.plugins.configfiles.custom;
     
     import com.cloudbees.plugins.credentials.common.IdCredentials;
    +import edu.umd.cs.findbugs.annotations.NonNull;
     import hudson.FilePath;
     import hudson.model.Run;
     import hudson.model.TaskListener;
    @@ -61,4 +62,10 @@ public String supplyContent(Config configFile, Run<?, ?> build, FilePath workDir
             }
             return fileContent;
         }
    +
    +    @Override
    +    public @NonNull List<String> getSensitiveContentForMasking(Config configFile, Run<?, ?> build) {
    +        HasCustomizedCredentialMappings settings = (HasCustomizedCredentialMappings) configFile;
    +        return CustomConfigCredentialsHelper.secretsForMasking(build, settings.getCustomizedCredentialMappings());
    +    }
     }
    
  • src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java+29 0 modified
    @@ -29,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal
     import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
     import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
     import com.cloudbees.plugins.credentials.domains.DomainRequirement;
    +import edu.umd.cs.findbugs.annotations.NonNull;
     import hudson.FilePath;
     import hudson.model.Run;
     import hudson.model.TaskListener;
    @@ -82,6 +83,34 @@ public static Map<String, IdCredentials> resolveCredentials(Run<?, ?> build, fin
             return customizedCredentialsMap;
         }
     
    +    public static @NonNull List<String> secretsForMasking(Run<?, ?> build, final List<CustomizedCredentialMapping> customizedCredentialMappings) {
    +        List<String> sensitiveStrings = new ArrayList<>();
    +        final Map<String, IdCredentials> resolveCredentials = resolveCredentials(build, customizedCredentialMappings, TaskListener.NULL);
    +        for (IdCredentials credential : resolveCredentials.values()) {
    +            // username is not used so no need to mask.
    +            if (credential instanceof StandardUsernamePasswordCredentials) {
    +                StandardUsernamePasswordCredentials supc = (StandardUsernamePasswordCredentials)credential;
    +                if (supc.isUsernameSecret()) {
    +                    sensitiveStrings.add(supc.getUsername());
    +                }
    +                sensitiveStrings.add(supc.getPassword().getPlainText());
    +            } else if (credential instanceof SSHUserPrivateKey) {
    +                SSHUserPrivateKey sshUserPrivateKey = (SSHUserPrivateKey) credential;
    +                if (sshUserPrivateKey.isUsernameSecret()) {
    +                    sensitiveStrings.add(sshUserPrivateKey.getUsername());
    +                }
    +                List<String> privateKeys = sshUserPrivateKey.getPrivateKeys();
    +                if (!sshUserPrivateKey.getPrivateKeys().isEmpty()) {
    +                    // only the first key is supported
    +                    sensitiveStrings.add(privateKeys.get(0));
    +                }
    +            } else if (credential instanceof StringCredentials) {
    +                sensitiveStrings.add(((StringCredentials)credential).getSecret().getPlainText());
    +            }
    +        }
    +        return sensitiveStrings;
    +    }
    +
         public static String fillAuthentication(Run<?, ?> build, FilePath workDir, TaskListener listener,
                 String customizedContent, Map<String, IdCredentials> customizedCredentialsMap)
                 throws MacroEvaluationException, IOException, InterruptedException {
    
  • src/main/java/org/jenkinsci/plugins/configfiles/maven/AbstractMavenSettingsProvider.java+8 0 modified
    @@ -28,6 +28,7 @@ of this software and associated documentation files (the "Software"), to deal
     import java.util.List;
     import java.util.Map;
     
    +import edu.umd.cs.findbugs.annotations.NonNull;
     import org.jenkinsci.lib.configprovider.AbstractConfigProviderImpl;
     import org.jenkinsci.lib.configprovider.model.Config;
     import org.jenkinsci.lib.configprovider.model.ContentType;
    @@ -79,4 +80,11 @@ public String supplyContent(Config configFile, Run<?, ?> build, FilePath workDir
             }
             return fileContent;
         }
    +
    +    @Override
    +    public @NonNull List<String> getSensitiveContentForMasking(Config configFile, Run<?, ?> build) {
    +        HasServerCredentialMappings settings = (HasServerCredentialMappings) configFile;
    +        return CredentialsHelper.secretsForMasking(build, settings.getServerCredentialMappings());
    +    }
    +
     }
    
  • src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnConsoleLogFilter.java+91 0 added
    @@ -0,0 +1,91 @@
    +/*
    + The MIT License
    +
    + Copyright (c) 2023, CloudBees Inc.
    +
    + Permission is hereby granted, free of charge, to any person obtaining a copy
    + of this software and associated documentation files (the "Software"), to deal
    + in the Software without restriction, including without limitation the rights
    + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    + copies of the Software, and to permit persons to whom the Software is
    + furnished to do so, subject to the following conditions:
    +
    + The above copyright notice and this permission notice shall be included in
    + all copies or substantial portions of the Software.
    +
    + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    + THE SOFTWARE.
    + */
    +package org.jenkinsci.plugins.configfiles.maven.job;
    +
    +import hudson.Extension;
    +import hudson.console.ConsoleLogFilter;
    +import hudson.model.AbstractBuild;
    +import hudson.model.AbstractProject;
    +import hudson.model.FreeStyleBuild;
    +import hudson.model.Run;
    +import hudson.util.Secret;
    +import jenkins.util.JenkinsJVM;
    +import org.apache.commons.beanutils.PropertyUtils;
    +import org.jenkinsci.plugins.credentialsbinding.masking.SecretPatterns;
    +
    +import java.io.IOException;
    +import java.io.OutputStream;
    +import java.io.PrintStream;
    +import java.lang.reflect.InvocationTargetException;
    +import java.util.ArrayList;
    +import java.util.Collections;
    +import java.util.List;
    +import java.util.function.Supplier;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
    +import java.util.regex.Pattern;
    +
    +/**
    + * Extension to mask any sensitive credentials provided by this plugin in maven settings (local or global) for the maven job type.
    + */
    +@Extension
    +public class MvnConsoleLogFilter extends ConsoleLogFilter {
    +
    +    private static final Logger LOGGER = Logger.getLogger(MvnConsoleLogFilter.class.getName());
    +
    +    @Override
    +    public OutputStream decorateLogger(Run build, OutputStream logger) throws IOException, InterruptedException {
    +        if (build instanceof AbstractBuild && !(build instanceof FreeStyleBuild)) {
    +            AbstractProject<?, ?> parent = (AbstractProject<?, ?>) build.getParent();
    +            if (parent.getClass().getSimpleName().equals("MavenModuleSet")) {
    +                List<String> secretValues = new ArrayList<>();
    +                try { //Maven
    +                    Object settings = PropertyUtils.getProperty(parent, "settings");
    +                    if (settings instanceof MvnSettingsProvider) {
    +                        MvnSettingsProvider provider = (MvnSettingsProvider) settings;
    +                        secretValues.addAll(provider.getSensitiveContentForMasking((AbstractBuild)build));
    +                    }
    +                    Object globalSettings = PropertyUtils.getProperty(parent, "globalSettings");
    +                    if (globalSettings instanceof MvnGlobalSettingsProvider) {
    +                        MvnGlobalSettingsProvider provider = (MvnGlobalSettingsProvider) globalSettings;
    +                        secretValues.addAll(provider.getSensitiveContentForMasking((AbstractBuild)build));
    +                    }
    +                } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
    +                    LOGGER.log(Level.WARNING, "Unable to mask secrets for " + parent.getFullName() + "#" + build.getNumber(), e);
    +                    PrintStream ps = new PrintStream(logger, false, build.getCharset());
    +                    e.printStackTrace(ps);
    +                    ps.flush();
    +                    assert false : "MavenModuleSet API has changed in an incompatable way";
    +                }
    +                if (!secretValues.isEmpty()) {
    +                    final Secret pattern = Secret.fromString(SecretPatterns.getAggregateSecretPattern(secretValues).pattern());
    +                    return new SecretPatterns.MaskingOutputStream(logger, 
    +                            () -> Pattern.compile(pattern.getPlainText()),
    +                            build.getCharset().name());
    +                }
    +            }
    +        }
    +        return logger;
    +    }
    +}
    
  • src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnGlobalSettingsProvider.java+65 39 modified
    @@ -11,6 +11,7 @@
     import hudson.model.AbstractBuild;
     
     import java.util.ArrayList;
    +import java.util.Collections;
     import java.util.List;
     import java.util.Map;
     import java.util.logging.Level;
    @@ -26,12 +27,17 @@
     import org.jenkinsci.plugins.configfiles.ConfigFiles;
     import org.jenkinsci.plugins.configfiles.common.CleanTempFilesAction;
     import org.jenkinsci.plugins.configfiles.maven.GlobalMavenSettingsConfig;
    +import org.jenkinsci.plugins.configfiles.maven.MavenSettingsConfig;
     import org.jenkinsci.plugins.configfiles.maven.GlobalMavenSettingsConfig.GlobalMavenSettingsConfigProvider;
     import org.jenkinsci.plugins.configfiles.maven.security.CredentialsHelper;
    +import org.jenkinsci.plugins.configfiles.maven.security.ServerCredentialMapping;
     import org.kohsuke.stapler.AncestorInPath;
     import org.kohsuke.stapler.DataBoundConstructor;
     
     import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
    +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
    +import edu.umd.cs.findbugs.annotations.CheckForNull;
    +import edu.umd.cs.findbugs.annotations.NonNull;
     import org.kohsuke.stapler.QueryParameter;
     
     /**
    @@ -66,67 +72,87 @@ public void setSettingsConfigId(String settingsConfigId) {
             this.settingsConfigId = settingsConfigId;
         }
     
    -    @Override
    -    public FilePath supplySettings(AbstractBuild<?, ?> build, TaskListener listener) {
    -        if (StringUtils.isNotBlank(settingsConfigId)) {
     
    +    @CheckForNull
    +    private GlobalMavenSettingsConfig getGlobalMavenSettingsConfig(AbstractBuild<?, ?> build, TaskListener listener) {
    +        if (StringUtils.isNotBlank(settingsConfigId)) {
                 Config c = ConfigFiles.getByIdOrNull(build.getRootBuild(), settingsConfigId);
     
                 if (c == null) {
                     String msg = "your Apache Maven build is setup to use a global config with id " + settingsConfigId + " but can not find the config";
                     listener.getLogger().println("ERROR: " + msg);
                     throw new IllegalStateException(msg);
                 } else {
    -
    -                GlobalMavenSettingsConfig config;
                     if (c instanceof GlobalMavenSettingsConfig) {
    -                    config = (GlobalMavenSettingsConfig) c;
    -                } else {
    -                    config = new GlobalMavenSettingsConfig(c.id, c.name, c.comment, c.content, GlobalMavenSettingsConfig.isReplaceAllDefault, null);
    +                    return (GlobalMavenSettingsConfig) c;
                     }
    +                return new GlobalMavenSettingsConfig(c.id, c.name, c.comment, c.content, MavenSettingsConfig.isReplaceAllDefault, null);
    +            }
    +        }
    +        return null;
    +    }
     
    -                listener.getLogger().println("using global settings config with name " + config.name);
    -                listener.getLogger().println("Replacing all maven server entries not found in credentials list is " + config.getIsReplaceAll());
    -                if (StringUtils.isNotBlank(config.content)) {
    -                    try {
    -
    -                        FilePath workspace = build.getWorkspace();
    -                        if (workspace != null) {
    -                            FilePath workDir = WorkspaceList.tempDir(workspace);
    -                            String fileContent = config.content;
    -
    -                            final Map<String, StandardUsernameCredentials> resolvedCredentials = CredentialsHelper.resolveCredentials(build, config.getServerCredentialMappings(), listener);
    -                            final Boolean isReplaceAll = config.getIsReplaceAll();
    -
    -                            if (resolvedCredentials != null && !resolvedCredentials.isEmpty()) {
    -                                List<String> tempFiles = new ArrayList<String>();
    -                                fileContent = CredentialsHelper.fillAuthentication(fileContent, isReplaceAll, resolvedCredentials, workDir, tempFiles);
    -                                for (String tempFile : tempFiles) {
    -                                    build.addAction(new CleanTempFilesAction(tempFile));
    -                                }
    +    @Override
    +    public FilePath supplySettings(AbstractBuild<?, ?> build, TaskListener listener) {
    +        GlobalMavenSettingsConfig config = getGlobalMavenSettingsConfig(build, listener);
    +        if (config != null) {
    +            listener.getLogger().println("using global settings config with name " + config.name);
    +            listener.getLogger().println("Replacing all maven server entries not found in credentials list is " + config.getIsReplaceAll());
    +            if (StringUtils.isNotBlank(config.content)) {
    +                try {
    +
    +                    FilePath workspace = build.getWorkspace();
    +                    if (workspace != null) {
    +                        FilePath workDir = WorkspaceList.tempDir(workspace);
    +                        String fileContent = config.content;
    +
    +                        final Map<String, StandardUsernameCredentials> resolvedCredentials = CredentialsHelper.resolveCredentials(build, config.getServerCredentialMappings(), listener);
    +                        final Boolean isReplaceAll = config.getIsReplaceAll();
    +
    +                        if (resolvedCredentials != null && !resolvedCredentials.isEmpty()) {
    +                            List<String> tempFiles = new ArrayList<String>();
    +                            fileContent = CredentialsHelper.fillAuthentication(fileContent, isReplaceAll, resolvedCredentials, workDir, tempFiles);
    +                            for (String tempFile : tempFiles) {
    +                                build.addAction(new CleanTempFilesAction(tempFile));
                                 }
    +                        }
     
    -                            FilePath configurationFile = workspace.createTextTempFile("global-settings", ".xml", fileContent, false);
    -                            LOGGER.log(Level.FINE, "Create {0}", new Object[]{configurationFile});
    -                            build.getEnvironments().add(new SimpleEnvironment("MVN_GLOBALSETTINGS", configurationFile.getRemote()));
    +                        FilePath configurationFile = workspace.createTextTempFile("global-settings", ".xml", fileContent, false);
    +                        LOGGER.log(Level.FINE, "Create {0}", new Object[]{configurationFile});
    +                        build.getEnvironments().add(new SimpleEnvironment("MVN_GLOBALSETTINGS", configurationFile.getRemote()));
     
    -                            // Temporarily attach info about the files to be deleted to the build - this action gets removed from the build again by
    -                            // 'org.jenkinsci.plugins.configfiles.common.CleanTempFilesRunListener'
    -                            build.addAction(new CleanTempFilesAction(configurationFile.getRemote()));
    -                            return configurationFile;
    -                        } else {
    -                            listener.getLogger().println("ERROR: can't supply maven settings, workspace is null / agent seems not connected...");
    -                        }
    -                    } catch (Exception e) {
    -                        throw new IllegalStateException("the global settings.xml could not be supplied for the current build: " + e.getMessage());
    +                        // Temporarily attach info about the files to be deleted to the build - this action gets removed from the build again by
    +                        // 'org.jenkinsci.plugins.configfiles.common.CleanTempFilesRunListener'
    +                        build.addAction(new CleanTempFilesAction(configurationFile.getRemote()));
    +                        return configurationFile;
    +                    } else {
    +                        listener.getLogger().println("ERROR: can't supply maven settings, workspace is null / agent seems not connected...");
                         }
    +                } catch (Exception e) {
    +                    throw new IllegalStateException("the global settings.xml could not be supplied for the current build: " + e.getMessage());
                     }
                 }
             }
     
             return null;
         }
     
    +    /**
    +     * Obtain a list of sensitive Strings to mask for the given provider and build.
    +     * For example if a {@link UsernamePasswordCredentials} credential is being
    +     * injected into the file then the password (and possibly the username) would need to be masked and should be returned here.
    +     * @return List of Strings that need to be masked in the console.
    +     */
    +    public @NonNull List<String> getSensitiveContentForMasking(AbstractBuild<?, ?> build) {
    +        GlobalMavenSettingsConfig config = getGlobalMavenSettingsConfig(build, TaskListener.NULL);
    +        if (config != null) {
    +            final List<ServerCredentialMapping> serverCredentialMappings = config.getServerCredentialMappings();
    +            final List<String> secretsForMasking = CredentialsHelper.secretsForMasking(build, serverCredentialMappings);
    +            return secretsForMasking;
    +        }
    +        return Collections.emptyList();
    +    }
    +
         @Extension(ordinal = 10)
         public static class DescriptorImpl extends GlobalSettingsProviderDescriptor {
     
    
  • src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnSettingsProvider.java+63 40 modified
    @@ -1,6 +1,7 @@
     package org.jenkinsci.plugins.configfiles.maven.job;
     
     import java.util.ArrayList;
    +import java.util.Collections;
     import java.util.List;
     import java.util.Map;
     import java.util.logging.Level;
    @@ -22,6 +23,9 @@
     import org.kohsuke.stapler.DataBoundConstructor;
     
     import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
    +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
    +import edu.umd.cs.findbugs.annotations.CheckForNull;
    +import edu.umd.cs.findbugs.annotations.NonNull;
     
     import hudson.Extension;
     import hudson.FilePath;
    @@ -66,9 +70,8 @@ public void setSettingsConfigId(String settingsConfigId) {
             this.settingsConfigId = settingsConfigId;
         }
     
    -    @Override
    -    public FilePath supplySettings(AbstractBuild<?, ?> build, TaskListener listener) {
    -
    +    @CheckForNull
    +    private MavenSettingsConfig getMavenSettingsConfig(AbstractBuild<?, ?> build, TaskListener listener) {
             if (StringUtils.isNotBlank(settingsConfigId)) {
     
                 Config c = ConfigFiles.getByIdOrNull(build.getRootBuild(), settingsConfigId);
    @@ -78,57 +81,77 @@ public FilePath supplySettings(AbstractBuild<?, ?> build, TaskListener listener)
                     listener.getLogger().println("ERROR: " + msg);
                     throw new IllegalStateException(msg);
                 } else {
    -
    -                MavenSettingsConfig config;
                     if (c instanceof MavenSettingsConfig) {
    -                    config = (MavenSettingsConfig) c;
    -                } else {
    -                    config = new MavenSettingsConfig(c.id, c.name, c.comment, c.content, MavenSettingsConfig.isReplaceAllDefault, null);
    +                    return (MavenSettingsConfig) c;
                     }
    +                return new MavenSettingsConfig(c.id, c.name, c.comment, c.content, MavenSettingsConfig.isReplaceAllDefault, null);
    +            }
    +        }
    +        return null;
    +    }
     
    -                listener.getLogger().println("using settings config with name " + config.name);
    -                listener.getLogger().println("Replacing all maven server entries not found in credentials list is " + config.getIsReplaceAll());
    -                if (StringUtils.isNotBlank(config.content)) {
    -                    try {
    -
    -                        FilePath workspace = build.getWorkspace();
    -                        if (workspace != null) {
    -                            FilePath workDir = WorkspaceList.tempDir(workspace);
    -                            String fileContent = config.content;
    -
    -                            final List<ServerCredentialMapping> serverCredentialMappings = config.getServerCredentialMappings();
    -                            final Map<String, StandardUsernameCredentials> resolvedCredentials = CredentialsHelper.resolveCredentials(build, serverCredentialMappings, listener);
    -                            final Boolean isReplaceAll = config.getIsReplaceAll();
    -
    -                            if (!resolvedCredentials.isEmpty()) {
    -                                List<String> tempFiles = new ArrayList<String>();
    -                                fileContent = CredentialsHelper.fillAuthentication(fileContent, isReplaceAll, resolvedCredentials, workDir, tempFiles);
    -                                for (String tempFile : tempFiles) {
    -                                    build.addAction(new CleanTempFilesAction(tempFile));
    -                                }
    +    @Override
    +    public FilePath supplySettings(AbstractBuild<?, ?> build, TaskListener listener) {
    +        MavenSettingsConfig config = getMavenSettingsConfig(build, listener);
    +        if (config != null) {
    +            listener.getLogger().println("using settings config with name " + config.name);
    +            listener.getLogger().println("Replacing all maven server entries not found in credentials list is " + config.getIsReplaceAll());
    +            if (StringUtils.isNotBlank(config.content)) {
    +                try {
    +
    +                    FilePath workspace = build.getWorkspace();
    +                    if (workspace != null) {
    +                        FilePath workDir = WorkspaceList.tempDir(workspace);
    +                        String fileContent = config.content;
    +
    +                        final List<ServerCredentialMapping> serverCredentialMappings = config.getServerCredentialMappings();
    +                        final Map<String, StandardUsernameCredentials> resolvedCredentials = CredentialsHelper.resolveCredentials(build, serverCredentialMappings, listener);
    +                        final Boolean isReplaceAll = config.getIsReplaceAll();
    +
    +                        if (!resolvedCredentials.isEmpty()) {
    +                            List<String> tempFiles = new ArrayList<String>();
    +                            fileContent = CredentialsHelper.fillAuthentication(fileContent, isReplaceAll, resolvedCredentials, workDir, tempFiles);
    +                            for (String tempFile : tempFiles) {
    +                                build.addAction(new CleanTempFilesAction(tempFile));
                                 }
    +                        }
     
    -                            final FilePath f = workspace.createTextTempFile("settings", ".xml", fileContent, false);
    -                            LOGGER.log(Level.FINE, "Create {0}", new Object[]{f});
    -                            build.getEnvironments().add(new SimpleEnvironment("MVN_SETTINGS", f.getRemote()));
    +                        final FilePath f = workspace.createTextTempFile("settings", ".xml", fileContent, false);
    +                        LOGGER.log(Level.FINE, "Create {0}", new Object[]{f});
    +                        build.getEnvironments().add(new SimpleEnvironment("MVN_SETTINGS", f.getRemote()));
     
    -                            // Temporarily attach info about the files to be deleted to the build - this action gets removed from the build again by
    -                            // 'org.jenkinsci.plugins.configfiles.common.CleanTempFilesRunListener'
    -                            build.addAction(new CleanTempFilesAction(f.getRemote()));
    -                            return f;
    -                        } else {
    -                            listener.getLogger().println("ERROR: can't supply maven settings, workspace is null / agent seems not connected...");
    -                        }
    -                    } catch (Exception e) {
    -                        throw new IllegalStateException("the settings.xml could not be supplied for the current build: " + e.getMessage(), e);
    +                        // Temporarily attach info about the files to be deleted to the build - this action gets removed from the build again by
    +                        // 'org.jenkinsci.plugins.configfiles.common.CleanTempFilesRunListener'
    +                        build.addAction(new CleanTempFilesAction(f.getRemote()));
    +                        return f;
    +                    } else {
    +                        listener.getLogger().println("ERROR: can't supply maven settings, workspace is null / agent seems not connected...");
                         }
    +                } catch (Exception e) {
    +                    throw new IllegalStateException("the settings.xml could not be supplied for the current build: " + e.getMessage(), e);
                     }
                 }
             }
     
             return null;
         }
     
    +    /**
    +     * Obtain a list of sensitive Strings to mask for the given provider and build.
    +     * For example if a {@link UsernamePasswordCredentials} credential is being
    +     * injected into the file then the password (and possibly the username) would need to be masked and should be returned here.
    +     * @return List of Strings that need to be masked in the console.
    +     */
    +    public @NonNull List<String> getSensitiveContentForMasking(AbstractBuild<?, ?> build) {
    +        MavenSettingsConfig config = getMavenSettingsConfig(build, TaskListener.NULL);
    +        if (config != null) {
    +            final List<ServerCredentialMapping> serverCredentialMappings = config.getServerCredentialMappings();
    +            final List<String> secretsForMasking = CredentialsHelper.secretsForMasking(build, serverCredentialMappings);
    +            return secretsForMasking;
    +        }
    +        return Collections.emptyList();
    +    }
    +
         @Extension(ordinal = 10)
         public static class DescriptorImpl extends SettingsProviderDescriptor {
     
    
  • src/main/java/org/jenkinsci/plugins/configfiles/maven/security/CredentialsHelper.java+26 1 modified
    @@ -3,6 +3,7 @@
     
     import java.io.StringReader;
     import java.io.StringWriter;
    +import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.Collection;
     import java.util.Collections;
    @@ -38,7 +39,7 @@
     import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
     import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
     import com.cloudbees.plugins.credentials.domains.DomainRequirement;
    -
    +import edu.umd.cs.findbugs.annotations.NonNull;
     import hudson.FilePath;
     import hudson.model.Run;
     import hudson.model.TaskListener;
    @@ -228,6 +229,30 @@ public static String fillAuthentication(String mavenSettingsContent, final Boole
             return content;
         }
     
    +    public static @NonNull List<String> secretsForMasking(Run<?, ?> build, List<ServerCredentialMapping> propertiesCredentialMappings) {
    +        List<String> sensitiveStrings = new ArrayList<>();
    +        final Map<String, StandardUsernameCredentials> resolveCredentials = resolveCredentials(build, propertiesCredentialMappings, TaskListener.NULL);
    +        for (StandardUsernameCredentials credential : resolveCredentials.values()) {
    +            if (credential.isUsernameSecret()) {
    +                sensitiveStrings.add(credential.getUsername());
    +            }
    +            if (credential instanceof StandardUsernamePasswordCredentials) {
    +                sensitiveStrings.add(((StandardUsernamePasswordCredentials) credential).getPassword().getPlainText());
    +            } else if (credential instanceof SSHUserPrivateKey) {
    +                SSHUserPrivateKey sshUserPrivateKey = (SSHUserPrivateKey) credential;
    +                Secret passphrase = sshUserPrivateKey.getPassphrase();
    +                if (passphrase != null && !passphrase.getPlainText().isBlank()) {
    +                    sensitiveStrings.add(passphrase.getPlainText());
    +                }
    +                // ssh private keys are on disk and not part of the settings file.
    +                // The actual private key is multi-line and not masked.
    +                // https://github.com/jenkinsci/credentials-binding-plugin/pull/92
    +                // the location of the key is not considered sensitive if leaked
    +            }
    +        }
    +        return sensitiveStrings;
    +    }
    +
         /*
          * Copy non credential attributes from a node to other
          */
    
  • src/main/java/org/jenkinsci/plugins/configfiles/properties/AbstractPropertiesProvider.java+8 1 modified
    @@ -1,6 +1,7 @@
     package org.jenkinsci.plugins.configfiles.properties;
     
     import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
    +import edu.umd.cs.findbugs.annotations.NonNull;
     import hudson.FilePath;
     import hudson.model.Run;
     import hudson.model.TaskListener;
    @@ -37,4 +38,10 @@ public String supplyContent(Config configFile, Run<?, ?> build, FilePath workDir
             }
             return fileContent;
         }
    -}
    \ No newline at end of file
    +
    +    @Override
    +    public @NonNull List<String> getSensitiveContentForMasking(Config configFile, Run<?, ?> build) {
    +        HasPropertyCredentialMappings settings = (HasPropertyCredentialMappings) configFile;
    +        return CredentialsHelper.secretsForMasking(build, settings.getPropertiesCredentialMappings());
    +    }
    +}
    
  • src/main/java/org/jenkinsci/plugins/configfiles/properties/security/CredentialsHelper.java+21 1 modified
    @@ -5,6 +5,7 @@
     import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
     import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
     import com.cloudbees.plugins.credentials.domains.DomainRequirement;
    +import edu.umd.cs.findbugs.annotations.NonNull;
     import hudson.model.Run;
     import hudson.model.TaskListener;
     import org.apache.commons.lang.StringUtils;
    @@ -111,4 +112,23 @@ private static String createCredentialBasedProperty(final String propertyKey, fi
             return propertyKey + "=" + propertyValue;
         }
     
    -}
    \ No newline at end of file
    +    public static @NonNull List<String> secretsForMasking(Run<?, ?> build, List<PropertiesCredentialMapping> propertiesCredentialMappings) {
    +        List<String> sensitiveStrings = new ArrayList<>();
    +        final Map<String, StandardUsernameCredentials> resolveCredentials = resolveCredentials(build, propertiesCredentialMappings, TaskListener.NULL);
    +        for (StandardUsernameCredentials credential : resolveCredentials.values()) {
    +            // username is not used so no need to mask.
    +            if (credential instanceof StandardUsernamePasswordCredentials) {
    +                sensitiveStrings.add(((StandardUsernamePasswordCredentials)credential).getPassword().getPlainText());
    +            } else if (credential instanceof SSHUserPrivateKey) {
    +                SSHUserPrivateKey sshUserPrivateKey = (SSHUserPrivateKey) credential;
    +                List<String> privateKeys = sshUserPrivateKey.getPrivateKeys();
    +                if (!sshUserPrivateKey.getPrivateKeys().isEmpty()) {
    +                    // only the first key is supported
    +                    sensitiveStrings.add(privateKeys.get(0));
    +                }
    +            }
    +        }
    +        return sensitiveStrings;
    +    }
    +
    +}
    
  • src/test/java/org/jenkinsci/plugins/configfiles/custom/CustomConfigTest.java+80 0 added
    @@ -0,0 +1,80 @@
    +package org.jenkinsci.plugins.configfiles.custom;
    +
    +import java.io.FileReader;
    +import java.io.IOException;
    +import java.util.Collections;
    +import java.util.List;
    +import org.apache.commons.io.IOUtils;
    +import org.jenkinsci.plugins.configfiles.GlobalConfigFiles;
    +import org.jenkinsci.plugins.configfiles.buildwrapper.ConfigFileBuildWrapper;
    +import org.jenkinsci.plugins.configfiles.buildwrapper.ManagedFile;
    +import org.jenkinsci.plugins.configfiles.custom.security.CustomizedCredentialMapping;
    +import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
    +import org.junit.Assert;
    +import org.junit.ClassRule;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.junit.rules.TemporaryFolder;
    +import org.jvnet.hudson.test.BuildWatcher;
    +import org.jvnet.hudson.test.Issue;
    +import org.jvnet.hudson.test.JenkinsRule;
    +import com.cloudbees.plugins.credentials.CredentialsScope;
    +import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
    +import hudson.Launcher;
    +import hudson.model.AbstractBuild;
    +import hudson.model.BuildListener;
    +import hudson.model.FreeStyleBuild;
    +import hudson.model.FreeStyleProject;
    +import hudson.model.Result;
    +import hudson.model.Cause.UserIdCause;
    +import hudson.tasks.Builder;
    +import hudson.util.Secret;
    +
    +public class CustomConfigTest {
    +
    +    @ClassRule
    +    public static BuildWatcher buildWatcher = new BuildWatcher();
    +
    +    @ClassRule
    +    public static TemporaryFolder folder = new TemporaryFolder();
    +
    +    @Rule
    +    public JenkinsRule r = new JenkinsRule();
    +
    +    @Test @Issue("SECURITY-3090")
    +    public void credentialsAreMaskedInTheConsole() throws Exception {
    +        SystemCredentialsProvider.getInstance().getCredentials().add(new StringCredentialsImpl(CredentialsScope.GLOBAL, "creds", "desc", Secret.fromString("s3cr3t")));
    +
    +        GlobalConfigFiles.get().save(new CustomConfig("my-id", "my-name", "my-comment", "${thecred}", List.of(new CustomizedCredentialMapping("thecred", "creds"))));
    +
    +        final FreeStyleProject p = r.createFreeStyleProject("free");
    +
    +        final ManagedFile mCustom = new ManagedFile("my-id", folder.newFile().toString(), null, true);
    +        ConfigFileBuildWrapper bw = new ConfigFileBuildWrapper(Collections.singletonList(mCustom));
    +        p.getBuildWrappersList().add(bw);
    +
    +        p.getBuildersList().add(new VerifyFileContentBuilder(mCustom.getTargetLocation(), "s3cr3t"));
    +
    +        FreeStyleBuild build = r.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, new UserIdCause()).get());
    +        r.assertLogNotContains("s3cr3t", build);
    +    }
    +
    +    private static final class VerifyFileContentBuilder extends Builder {
    +        private final String filePath;
    +        private final String expectedContent;
    +
    +        public VerifyFileContentBuilder(String filePath, String expectedContent) {
    +            this.filePath = filePath;
    +            this.expectedContent = expectedContent;
    +        }
    +
    +        @Override
    +        public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
    +            final String fileContent = IOUtils.toString(new FileReader(filePath));
    +            listener.getLogger().println("File contents: ");
    +            listener.getLogger().println(fileContent);
    +            Assert.assertEquals("file content not correct", expectedContent, fileContent);
    +            return true;
    +        }
    +    }
    +}
    
  • src/test/java/org/jenkinsci/plugins/configfiles/maven/job/MvnSettingsCredentialsTest.java+46 4 modified
    @@ -5,9 +5,13 @@
     import com.cloudbees.plugins.credentials.CredentialsStore;
     import com.cloudbees.plugins.credentials.domains.Domain;
     import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
    +import hudson.EnvVars;
     import hudson.FilePath;
    +import hudson.Launcher;
     import hudson.maven.MavenModuleSet;
    +import hudson.maven.MavenModuleSetBuild;
     import hudson.model.AbstractBuild;
    +import hudson.model.BuildListener;
     import hudson.model.Cause.UserIdCause;
     import hudson.model.Result;
     import hudson.model.TaskListener;
    @@ -21,28 +25,35 @@
     import org.junit.Assert;
     import org.junit.Rule;
     import org.junit.Test;
    +import org.jvnet.hudson.test.BuildWatcher;
     import org.jvnet.hudson.test.ExtractResourceSCM;
    +import org.jvnet.hudson.test.Issue;
     import org.jvnet.hudson.test.JenkinsRule;
    +import org.jvnet.hudson.test.TestBuilder;
     import org.jvnet.hudson.test.ToolInstallations;
     
     import javax.inject.Inject;
     import java.io.File;
     import java.io.IOException;
     import java.util.ArrayList;
     import java.util.List;
    +import static org.junit.Assert.assertNotNull;
     
     public class MvnSettingsCredentialsTest {
     
         @Rule
         public JenkinsRule                        j = new JenkinsRule();
     
    +    @Rule
    +    public BuildWatcher                       bw = new BuildWatcher();
    +
         @Inject
         private MavenSettingsConfigProvider       mavenSettingProvider;
     
         @Inject
         private GlobalMavenSettingsConfigProvider globalMavenSettingsConfigProvider;
     
    -    @Test
    +    @Test @Issue("SECURITY-3090")
         public void serverCredentialsMustBeInSettingsXmlAtRuntime() throws Exception {
             j.jenkins.getInjector().injectMembers(this);
     
    @@ -63,14 +74,39 @@ public void serverCredentialsMustBeInSettingsXmlAtRuntime() throws Exception {
             p.setSettings(delegator);
             p.setGlobalSettings(delegator2);
     
    -        j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, new UserIdCause()).get());
    +        //SECURITY-3090
    +        p.getPostbuilders().add(new TestBuilder() {
    +            @Override
    +            public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
    +                EnvVars environment = build.getEnvironment(listener);
    +                String settingsPath = environment.get("MVN_SETTINGS");
    +                assertNotNull("There should be an environment var specifying the mvn settings", settingsPath);
    +                String globalSettingsPath = environment.get("MVN_GLOBALSETTINGS");
    +                assertNotNull("There should be an environment var specifying the mvn global settings", globalSettingsPath);
    +                String settings = build.getWorkspace().child(settingsPath).readToString();
    +                listener.getLogger().println("SETTINGS:\n" + settings);
    +                String globalSettings = build.getWorkspace().child(globalSettingsPath).readToString();
    +                listener.getLogger().println("GLOBAL SETTINGS:\n" + globalSettings);
    +                return true;
    +            }
    +        });
    +
    +        MavenModuleSetBuild build = j.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0, new UserIdCause()).get());
    +
    +        j.assertLogNotContains("<password>bar</password>", build);
    +        j.assertLogNotContains("<username>foo</username>", build);
    +        j.assertLogNotContains("<password>dudepwd</password>", build);
    +        j.assertLogNotContains("<username>dude</username>", build);
    +        j.assertLogContains("<username>****</username>", build);
    +        j.assertLogContains("<password>****</password>", build);
         }
     
         private static final class DelegatingMvnSettingsProvider extends MvnSettingsProvider {
     
             private final MvnSettingsProvider mvnSettingsProvider;
     
             public DelegatingMvnSettingsProvider(MvnSettingsProvider mvnSettingsProvider) {
    +            super(mvnSettingsProvider.getSettingsConfigId());
                 this.mvnSettingsProvider = mvnSettingsProvider;
             }
     
    @@ -87,13 +123,15 @@ public FilePath supplySettings(AbstractBuild<?, ?> build, TaskListener listener)
                 Assert.assertTrue("settings must contain password", settingContent.contains("<password>bar</password>"));
                 return settingsPath;
             }
    +
         }
     
         private static final class DelegatingGlobalMvnSettingsProvider extends MvnGlobalSettingsProvider {
     
             private final MvnGlobalSettingsProvider mvnSettingsProvider;
     
             public DelegatingGlobalMvnSettingsProvider(MvnGlobalSettingsProvider mvnSettingsProvider) {
    +            super(mvnSettingsProvider.getSettingsConfigId());
                 this.mvnSettingsProvider = mvnSettingsProvider;
             }
     
    @@ -115,7 +153,9 @@ public FilePath supplySettings(AbstractBuild<?, ?> build, TaskListener listener)
         private MavenSettingsConfig createSettings(MavenSettingsConfigProvider provider) throws Exception {
     
             CredentialsStore store = CredentialsProvider.lookupStores(j.getInstance()).iterator().next();
    -        store.addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "credid", "dummy desc", "foo", "bar"));
    +        UsernamePasswordCredentialsImpl credentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "credid", "dummy desc", "foo", "bar");
    +        credentials.setUsernameSecret(true);
    +        store.addCredentials(Domain.global(), credentials);
     
             ServerCredentialMapping mapping = new ServerCredentialMapping("myserver", "credid");
             List<ServerCredentialMapping> mappings = new ArrayList<ServerCredentialMapping>();
    @@ -133,7 +173,9 @@ private MavenSettingsConfig createSettings(MavenSettingsConfigProvider provider)
         private GlobalMavenSettingsConfig createGlobalSettings(GlobalMavenSettingsConfigProvider provider) throws Exception {
     
             CredentialsStore store = CredentialsProvider.lookupStores(j.getInstance()).iterator().next();
    -        store.addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "dudecredid", "dummy desc", "dude", "dudepwd"));
    +        UsernamePasswordCredentialsImpl credentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "dudecredid", "dummy desc", "dude", "dudepwd");
    +        credentials.setUsernameSecret(true);
    +        store.addCredentials(Domain.global(), credentials);
     
             ServerCredentialMapping mapping = new ServerCredentialMapping("someserver", "dudecredid");
             List<ServerCredentialMapping> mappings = new ArrayList<ServerCredentialMapping>();
    
  • src/test/java/org/jenkinsci/plugins/configfiles/maven/MavenSettingsConfigTest.java+107 6 modified
    @@ -1,7 +1,7 @@
     /*
      * The MIT License
      *
    - * Copyright 2017 CloudBees, Inc.
    + * Copyright 2017-2023 CloudBees, Inc.
      *
      * Permission is hereby granted, free of charge, to any person obtaining a copy
      * of this software and associated documentation files (the "Software"), to deal
    @@ -26,17 +26,44 @@
     import com.cloudbees.plugins.credentials.CredentialsScope;
     import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
     import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
    +
    +import java.io.IOException;
    +import java.text.MessageFormat;
     import java.util.Collections;
    +import java.util.List;
    +import java.util.logging.Level;
    +import java.util.logging.LogRecord;
    +import java.util.logging.Logger;
    +import hudson.Launcher;
    +import hudson.model.AbstractBuild;
    +import hudson.model.BuildListener;
    +import hudson.model.Computer;
    +import hudson.model.FreeStyleBuild;
    +import hudson.model.FreeStyleProject;
    +import hudson.remoting.Callable;
    +import hudson.slaves.DumbSlave;
     import org.jenkinsci.plugins.configfiles.GlobalConfigFiles;
    +import org.jenkinsci.plugins.configfiles.buildwrapper.ConfigFileBuildWrapper;
    +import org.jenkinsci.plugins.configfiles.buildwrapper.ManagedFile;
     import org.jenkinsci.plugins.configfiles.maven.security.ServerCredentialMapping;
    +import org.jenkinsci.plugins.credentialsbinding.masking.SecretPatterns;
     import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
     import org.jenkinsci.plugins.workflow.job.WorkflowJob;
     import org.jenkinsci.plugins.workflow.job.WorkflowRun;
    +import org.jenkinsci.remoting.RoleChecker;
    +import org.junit.Before;
     import org.junit.ClassRule;
     import org.junit.Test;
     import org.junit.Rule;
     import org.jvnet.hudson.test.BuildWatcher;
    +import org.jvnet.hudson.test.Issue;
     import org.jvnet.hudson.test.JenkinsRule;
    +import org.jvnet.hudson.test.LoggerRule;
    +import org.jvnet.hudson.test.TestBuilder;
    +
    +import static org.hamcrest.MatcherAssert.assertThat;
    +import static org.hamcrest.Matchers.containsString;
    +import static org.hamcrest.Matchers.not;
     
     public class MavenSettingsConfigTest {
     
    @@ -46,19 +73,93 @@ public class MavenSettingsConfigTest {
         @Rule
         public JenkinsRule r = new JenkinsRule();
     
    -    @Test
    +    public void assertNoSecretPatternOnControllerLog(Computer computer) throws IOException {
    +        // https://github.com/jenkinsci/credentials-binding-plugin/blob/8c080630bc65ef1eb1183d0e5cc179dc486122c0/src/main/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatterns.java#L82
    +        // brittle however the log is on the agent so LoggerRule will not work here. 
    +        assertThat(computer.getLog(), not(containsString("An agent attempted to look up secret patterns from the controller")));
    +    }
    +
    +    private UsernamePasswordCredentialsImpl createCredential(String id, String username, String password) {
    +        UsernamePasswordCredentialsImpl credentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, id, "", username, password);
    +        credentials.setUsernameSecret(true);
    +        SystemCredentialsProvider.getInstance().getCredentials().add(credentials);
    +        return credentials;
    +    }
    +    @Before
    +    public void before() {
    +        GlobalConfigFiles.get().save(new MavenSettingsConfig("m2settings", "m2settings", "", "<settings/>", true, 
    +                Collections.singletonList(new ServerCredentialMapping("myserver", createCredential("creds", "bot", "s3cr3t").getId()))));
    +        GlobalConfigFiles.get().save(new GlobalMavenSettingsConfig("m2GlobalSettings", "m2GlobalSettings", "", "<settings/>", true, 
    +                Collections.singletonList(new ServerCredentialMapping("myGlobalServer", createCredential("creds2", "admin", "sensitive").getId()))));
    +
    +    }
    +
    +    @Test @Issue("SECURITY-3090")
         public void withCredentials() throws Exception {
             // Smokes:
    -        SystemCredentialsProvider.getInstance().getCredentials().add(new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "creds", "", "bot", "s3cr3t"));
    -        GlobalConfigFiles.get().save(new MavenSettingsConfig("m2settings", "m2settings", "", "<settings/>", true, Collections.singletonList(new ServerCredentialMapping("myserver", "creds"))));
             WorkflowJob p = r.createProject(WorkflowJob.class, "p");
    -        p.setDefinition(new CpsFlowDefinition("node {configFileProvider([configFile(fileId: 'm2settings', variable: 'SETTINGS')]) {echo readFile(env.SETTINGS)}}", true));
    -        r.assertLogContains("<password>s3cr3t</password>", r.buildAndAssertSuccess(p));
    +        p.setDefinition(new CpsFlowDefinition(String.join("\n",
    +                "node () {",
    +                "  configFileProvider([configFile(fileId: 'm2settings', variable: 'SETTINGS'), configFile(fileId: 'm2GlobalSettings', variable: 'GOBAL_SETTINGS')]) {",
    +                "    String settings = readFile(env.SETTINGS)",
    +                "    echo settings",
    +                "    if (!settings.equals('<settings/>')) {", //Build #2 won't have credentials to assert on
    +                "      assert settings.contains('<password>s3cr3t</password>')",
    +                "      assert settings.contains('<username>bot</username>')",
    +                "    }",
    +                "    settings = readFile(env.GOBAL_SETTINGS)",
    +                "    echo settings",
    +                "    if (!settings.equals('<settings/>')) {", //Build #2 won't have credentials to assert on
    +                "      assert settings.contains('<password>sensitive</password>')",
    +                "      assert settings.contains('<username>admin</username>')",
    +                "    }",
    +                "  }",
    +                "}"), true));
    +        WorkflowRun run = r.buildAndAssertSuccess(p);
    +        r.assertLogNotContains("<password>s3cr3t</password>", run);
    +        r.assertLogNotContains("<username>bot</username>", run);
    +        r.assertLogNotContains("<password>sensitive</password>", run);
    +        r.assertLogNotContains("<username>admin</username>", run);
    +        r.assertLogContains("<password>****</password>", run);
    +        r.assertLogContains("<username>****</username>", run);
             // Missing credentials. Currently treated as nonfatal:
             SystemCredentialsProvider.getInstance().getCredentials().set(0, new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, "creds", "", "bot", "s3cr3t"));
    +        SystemCredentialsProvider.getInstance().getCredentials().set(1, new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, "creds2", "", "admin", "sensitive"));
             WorkflowRun b2 = r.buildAndAssertSuccess(p);
             r.assertLogContains("Could not find credentials [creds] for p #2", b2);
             r.assertLogNotContains("<password>", b2);
         }
     
    +    @Test @Issue("SECURITY-3090")
    +    public void freestyleWithCredentials() throws Exception {
    +        DumbSlave slave = r.createOnlineSlave();
    +
    +        FreeStyleProject p = r.createFreeStyleProject();
    +        p.getBuildWrappersList().add(new ConfigFileBuildWrapper(List.of(new ManagedFile("m2settings", null, "SETTINGS"), new ManagedFile("m2GlobalSettings", null, "GLOBAL_SETTINGS"))));
    +        p.getBuildersList().add(new TestBuilder() {
    +            @Override
    +            public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
    +                String settings = build.getWorkspace().child(build.getEnvironment(listener).get("SETTINGS")).readToString();
    +                listener.getLogger().println(settings);
    +                assertThat(settings, containsString("<password>s3cr3t</password>"));
    +                assertThat(settings, containsString("<username>bot</username>"));
    +
    +                
    +                settings = build.getWorkspace().child(build.getEnvironment(listener).get("GLOBAL_SETTINGS")).readToString();
    +                listener.getLogger().println(settings);
    +                assertThat(settings, containsString("<password>sensitive</password>"));
    +                assertThat(settings, containsString("<username>admin</username>"));
    +                return true;
    +            }
    +        });
    +        p.setAssignedNode(slave);
    +        FreeStyleBuild run = r.buildAndAssertSuccess(p);
    +        r.assertLogNotContains("<password>s3cr3t</password>", run);
    +        r.assertLogNotContains("<username>bot</username>", run);
    +        r.assertLogNotContains("<password>sensitive</password>", run);
    +        r.assertLogNotContains("<username>admin</username>", run);
    +        r.assertLogContains("<password>****</password>", run);
    +        r.assertLogContains("<username>****</username>", run);
    +    }
    +
     }
    
  • src/test/java/org/jenkinsci/plugins/configfiles/properties/PropertiesConfigTest.java+18 4 modified
    @@ -30,12 +30,26 @@ public void withCredentials() throws Exception {
             SystemCredentialsProvider.getInstance().getCredentials().add(new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "creds", "", "bot", "s3cr3t"));
             GlobalConfigFiles.get().save(new PropertiesConfig("gradle", "gradle", "", "myprop=", true, Collections.singletonList(new PropertiesCredentialMapping("myprop", "creds"))));
             WorkflowJob p = r.createProject(WorkflowJob.class, "p");
    -        p.setDefinition(new CpsFlowDefinition("node {configFileProvider([configFile(fileId: 'gradle', variable: 'SETTINGS')]) {echo readFile(env.SETTINGS)}}", true));
    -        r.assertLogContains("myprop=s3cr3t", r.buildAndAssertSuccess(p));
    +        p.setDefinition(new CpsFlowDefinition(
    +                String.join("\n", 
    +                  "node {",
    +                  "  configFileProvider([configFile(fileId: 'gradle', ",
    +                  "                                 variable: 'SETTINGS')]) {",
    +                  "    String content = readFile(env.SETTINGS)",
    +                  "    if (currentBuild.id == 1) { // only the first build will have the secret" ,
    +                  "      assert content.contains('myprop=s3cr3t')",
    +                  "    }",
    +                  "    echo content",
    +                  "  }",
    +                  "}"),
    +                true));
    +        WorkflowRun b1 = r.buildAndAssertSuccess(p);
    +        r.assertLogContains("myprop=****", b1);
    +        r.assertLogNotContains("myprop=s3cr3t", b1);
             // Missing credentials. Currently treated as nonfatal:
             SystemCredentialsProvider.getInstance().getCredentials().set(0, new UsernamePasswordCredentialsImpl(CredentialsScope.SYSTEM, "creds", "", "bot", "s3cr3t"));
             WorkflowRun b2 = r.buildAndAssertSuccess(p);
             r.assertLogContains("Could not find credentials [creds] for p #2", b2);
    -        r.assertLogNotContains("myprop=s3cr3t", b2);
    +        r.assertLogContains("myprop="+System.lineSeparator(), b2);
         }
    -}
    \ No newline at end of file
    +}
    

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