VYPR
Moderate severityNVD Advisory· Published Jul 31, 2019· Updated Aug 4, 2024

CVE-2019-10343

CVE-2019-10343

Description

Jenkins Configuration as Code Plugin 1.24 and earlier did not properly apply masking to values expected to be hidden when logging the configuration being applied.

AI Insight

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

Jenkins Configuration as Code Plugin 1.24 and earlier fails to mask sensitive values when logging configuration during application.

Vulnerability

Analysis

The Jenkins Configuration as Code Plugin, in versions 1.24 and earlier, contains a vulnerability where values that are expected to be hidden (masked) are not properly obscured when the plugin logs the configuration being applied [1][4]. This occurs because the masking function is not correctly applied to sensitive fields during the logging process, potentially exposing secrets in plaintext.

Exploitation

An attacker with access to Jenkins system logs could exploit this vulnerability by reading the log output generated when the Configuration as Code Plugin applies a configuration [2]. No special authentication or network position is required beyond the ability to view Jenkins controller logs. The issue is triggered automatically whenever a configuration is applied, making it a passive information disclosure risk.

Impact

The impact is the exposure of sensitive configuration values, such as passwords, API tokens, or other credentials, that were intended to be masked. This could allow an attacker to gain unauthorized access to connected systems or escalate privileges within Jenkins itself.

Mitigation

The vulnerability is fixed in version 1.25 of the Configuration as Code Plugin [3]. Users are advised to upgrade immediately. There is no known workaround, and the plugin's logs should be considered sensitive until the update is applied.

AI Insight generated on May 22, 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
io.jenkins:configuration-as-codeMaven
< 1.251.25

Affected products

2

Patches

1
73afe3cb10a7

[SECURITY-1303][SECURITY-1279]

13 files changed · +702 28
  • integrations/pom.xml+14 0 modified
    @@ -53,6 +53,13 @@
           <version>1.20</version>
           <scope>test</scope>
         </dependency>
    +    <dependency>
    +      <groupId>org.jenkins-ci.plugins</groupId>
    +      <artifactId>email-ext</artifactId>
    +      <version>2.66</version>
    +      <scope>test</scope>
    +    </dependency>
    +    
     
         <dependency>
           <groupId>org.jenkins-ci.plugins</groupId>
    @@ -101,6 +108,13 @@
           <scope>test</scope>
         </dependency>
     
    +    <dependency>
    +      <groupId>org.jenkins-ci.plugins</groupId>
    +      <artifactId>ssh-credentials</artifactId>
    +      <version>1.17</version>
    +      <scope>test</scope>
    +    </dependency>
    +
         <dependency>
           <groupId>org.jenkins-ci.plugins</groupId>
           <artifactId>github-branch-source</artifactId>
    
  • integrations/src/test/java/io/jenkins/plugins/casc/CredentialsTest.java+27 0 modified
    @@ -2,26 +2,33 @@
     
     import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey;
     import com.cloudbees.plugins.credentials.CredentialsProvider;
    +import com.cloudbees.plugins.credentials.CredentialsScope;
     import com.cloudbees.plugins.credentials.casc.CredentialsRootConfigurator;
     import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
    +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
     import hudson.ExtensionList;
     import hudson.util.Secret;
    +import io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator;
     import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
     import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
     import io.jenkins.plugins.casc.model.CNode;
     import io.jenkins.plugins.casc.model.Mapping;
     import java.util.Collections;
     import java.util.List;
    +import java.util.Set;
    +import javax.annotation.Nonnull;
     import jenkins.model.Jenkins;
     import org.junit.Rule;
     import org.junit.Test;
    +import org.jvnet.hudson.test.Issue;
     
     import static java.util.Objects.requireNonNull;
     import static org.hamcrest.Matchers.is;
     import static org.hamcrest.core.IsNot.not;
     import static org.junit.Assert.assertEquals;
     import static org.junit.Assert.assertNotNull;
     import static org.junit.Assert.assertThat;
    +import static org.junit.Assert.assertTrue;
     
     public class CredentialsTest {
     
    @@ -99,4 +106,24 @@ public void testExportSSHCredentials() throws Exception {
             assertThat(requireNonNull(Secret.decrypt(sshKeyExported)).getPlainText(), is("sp0ds9d+skkfjf"));
         }
     
    +    @Test
    +    @Issue("SECURITY-1404")
    +    public void checkUsernamePasswordIsSecret() {
    +        Attribute a = getFromDatabound(UsernamePasswordCredentialsImpl.class, "password");
    +        assertTrue("Attribute 'password' should be secret", a.isSecret(
    +                new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "1", "2", "3", "4")));
    +    }
    +
    +    @Nonnull
    +    private <T> Attribute<T,?> getFromDatabound(Class<T> clazz, @Nonnull String attributeName) {
    +        DataBoundConfigurator<T> cfg = new DataBoundConfigurator<T>(clazz);
    +        Set<Attribute<T,?>> attributes = cfg.describe();
    +        for (Attribute<T,?> a : attributes) {
    +            if(attributeName.equals(a.getName())) {
    +                return a;
    +            }
    +        }
    +        throw new AssertionError("Cannot find databound attribute " + attributeName + " in " + clazz);
    +    }
    +
     }
    
  • integrations/src/test/java/io/jenkins/plugins/casc/MailExtTest.java+46 0 added
    @@ -0,0 +1,46 @@
    +package io.jenkins.plugins.casc;
    +
    +import hudson.plugins.emailext.ExtendedEmailPublisher;
    +import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
    +import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.junit.rules.RuleChain;
    +import org.jvnet.hudson.test.Issue;
    +import org.jvnet.hudson.test.LoggerRule;
    +
    +import static io.jenkins.plugins.casc.misc.Util.assertLogContains;
    +import static io.jenkins.plugins.casc.misc.Util.assertNotInLog;
    +import static org.hamcrest.CoreMatchers.containsString;
    +import static org.hamcrest.CoreMatchers.not;
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertThat;
    +
    +public class MailExtTest {
    +
    +    public JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule();
    +    public LoggerRule logging = new LoggerRule();
    +
    +    @Rule
    +    public RuleChain chain= RuleChain
    +            .outerRule(logging.record(Logger.getLogger(Attribute.class.getName()), Level.INFO).capture(2048))
    +            .around(j);
    +
    +    private static final String SMTP_PASSWORD = "myPassword";
    +
    +    @Test
    +    @ConfiguredWithCode("MailExtTest.yml")
    +    @Issue("SECURITY-1404")
    +    public void shouldNotExportOrLogCredentials() throws Exception {
    +        assertEquals(SMTP_PASSWORD, ExtendedEmailPublisher.descriptor().getSmtpPassword().getPlainText());
    +        assertLogContains(logging, "smtpPassword =");
    +        assertNotInLog(logging, SMTP_PASSWORD);
    +
    +        // Verify that the password does not get exported
    +        String exportedConfig = j.exportToString(false);
    +        assertThat("No entry was exported for SMTP credentials", exportedConfig, containsString("smtpPassword"));
    +        assertThat("There should be no SMTP password in the exported YAML", exportedConfig, not(containsString(SMTP_PASSWORD)));
    +    }
    +}
    
  • integrations/src/test/java/io/jenkins/plugins/casc/SSHCredentialsTest.java+66 0 added
    @@ -0,0 +1,66 @@
    +package io.jenkins.plugins.casc;
    +
    +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey;
    +import com.cloudbees.plugins.credentials.Credentials;
    +import com.cloudbees.plugins.credentials.CredentialsProvider;
    +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
    +import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
    +import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
    +import java.util.Collections;
    +import java.util.List;
    +import java.util.logging.Level;
    +import jenkins.model.Jenkins;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.junit.rules.RuleChain;
    +import org.jvnet.hudson.test.Issue;
    +import org.jvnet.hudson.test.LoggerRule;
    +
    +import static io.jenkins.plugins.casc.misc.Util.assertNotInLog;
    +import static org.hamcrest.CoreMatchers.containsString;
    +import static org.hamcrest.CoreMatchers.not;
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertThat;
    +
    +/**
    + * Integration tests for the SSH Credentials Plugin.
    + */
    +public class SSHCredentialsTest {
    +
    +    public JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule();
    +    public LoggerRule logging = new LoggerRule();
    +
    +    @Rule
    +    public RuleChain chain= RuleChain
    +            .outerRule(logging.record("io.jenkins.plugins.casc.Attribute", Level.INFO).capture(2048))
    +            .around(j);
    +
    +    private static final String CREDENTIALS_PASSWORD = "password-of-userid";
    +    private static final String PRIVATE_KEY = "sp0ds9d+skkfjf";
    +
    +    @Test
    +    @ConfiguredWithCode("SSHCredentialsTest.yml")
    +    @Issue("SECURITY-1279")
    +    public void shouldNotExportOrLogCredentials() throws Exception {
    +        StandardUsernamePasswordCredentials creds = getCredentials(StandardUsernamePasswordCredentials.class);
    +        assertEquals(CREDENTIALS_PASSWORD, creds.getPassword().getPlainText());
    +        assertNotInLog(logging, CREDENTIALS_PASSWORD);
    +
    +        BasicSSHUserPrivateKey certKey = getCredentials(BasicSSHUserPrivateKey.class);
    +        assertEquals(PRIVATE_KEY, certKey.getPrivateKey());
    +        assertNotInLog(logging, PRIVATE_KEY);
    +
    +        // Verify that the password does not get exported
    +        String exportedConfig = j.exportToString(false);
    +        assertThat("There should be no password in the exported YAML", exportedConfig, not(containsString(CREDENTIALS_PASSWORD)));
    +        assertThat("There should be no private key in the exported YAML", exportedConfig, not(containsString(PRIVATE_KEY)));
    +    }
    +
    +    private <T extends Credentials> T getCredentials(Class<T> clazz) {
    +        List<T> creds = CredentialsProvider.lookupCredentials(
    +                clazz, Jenkins.getInstanceOrNull(),
    +                null, Collections.emptyList());
    +        assertEquals(1, creds.size());
    +        return (T)creds.get(0);
    +    }
    +}
    
  • integrations/src/test/resources/io/jenkins/plugins/casc/MailExtTest.yml+7 0 added
    @@ -0,0 +1,7 @@
    +unclassified:
    +  extendedEmailPublisher:
    +    smtpUsername: email@acmecorp.com
    +    smtpPassword: myPassword
    +    defaultContentType: text/plain
    +    defaultSubject: "Build $BUILD_NUMBER - $BUILD_STATUS"
    +    defaultBody: "Check console output at $BUILD_URL"
    
  • integrations/src/test/resources/io/jenkins/plugins/casc/SSHCredentialsTest.yml+21 0 added
    @@ -0,0 +1,21 @@
    +jenkins:
    +  systemMessage: Jenkins with SSH Credentials for JCasC test
    +
    +credentials:
    +  system:
    +    domainCredentials:
    +      - credentials:
    +          - usernamePassword:
    +              scope: SYSTEM
    +              id: "userid"
    +              username: "username-of-userid"
    +              password: "password-of-userid"
    +          - basicSSHUserPrivateKey:
    +              scope: SYSTEM
    +              id: "userid2"
    +              username: "username-of-userid2"
    +              passphrase: "passphrase-of-userid2"
    +              description: "the description of userid2"
    +              privateKeySource:
    +                directEntry:
    +                  privateKey: "sp0ds9d+skkfjf"
    
  • plugin/src/main/java/io/jenkins/plugins/casc/Attribute.java+124 22 modified
    @@ -1,9 +1,11 @@
     package io.jenkins.plugins.casc;
     
    +import com.google.common.annotations.VisibleForTesting;
     import hudson.util.Secret;
     import io.jenkins.plugins.casc.model.CNode;
     import io.jenkins.plugins.casc.model.Scalar;
     import io.jenkins.plugins.casc.model.Sequence;
    +import io.jenkins.plugins.casc.util.ExtraFieldUtils;
     import java.lang.reflect.Array;
     import java.lang.reflect.Field;
     import java.lang.reflect.InvocationTargetException;
    @@ -20,9 +22,10 @@
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import java.util.stream.Collectors;
    +import javax.annotation.CheckForNull;
    +import javax.annotation.Nonnull;
     import org.apache.commons.collections.CollectionUtils;
     import org.apache.commons.lang.StringUtils;
    -import org.apache.commons.lang.reflect.FieldUtils;
     import org.kohsuke.accmod.AccessRestriction;
     import org.kohsuke.stapler.export.Exported;
     
    @@ -40,12 +43,17 @@ public class Attribute<Owner, Type> {
     
         private static final Logger LOGGER = Logger.getLogger(Attribute.class.getName());
     
    +    //TODO: Concurrent cache?
    +    //private static final HashMap<Class, Boolean> SECRET_ATTRIBUTE_CACHE =
    +    //        new HashMap<>();
    +
         protected final String name;
         protected final Class type;
         protected boolean multiple;
         protected String preferredName;
         private Setter<Owner, Type> setter;
         private Getter<Owner, Type> getter;
    +    private boolean secret;
     
         private boolean deprecated;
     
    @@ -60,6 +68,7 @@ public Attribute(String name, Class type) {
             this.setter = this::_setValue;
             this.aliases = new ArrayList<>();
             this.aliases.add(name);
    +        this.secret = type == Secret.class || calculateIfSecret(null, this.name);
         }
     
         @SuppressWarnings("unchecked")
    @@ -134,6 +143,17 @@ public Attribute<Owner, Type> getter(Getter<Owner, Type> getter) {
             return this;
         }
     
    +    /**
    +     * Sets whether the attribute is secret.
    +     * If so, various outputs will be suppressed (exports, logging).
    +     * @param secret {@code true} to make an attribute secret
    +     * @since TODO
    +     */
    +    public Attribute<Owner, Type> secret(boolean secret) {
    +        this.secret = secret;
    +        return this;
    +    }
    +
         public Attribute deprecated(boolean deprecated) {
             this.deprecated = deprecated;
             return this;
    @@ -170,9 +190,21 @@ public List<String> possibleValues() {
             return Collections.EMPTY_LIST;
         }
     
    +    /**
    +     * Checks whether an attribute is considered a secret one.
    +     * @return {@code true} if the attribute is secret
    +     * @param target Target object.
    +     *               If {@code null}, only the attribute metadata is checked
    +     * @since TODO
    +     */
    +    public boolean isSecret(@CheckForNull Owner target) {
    +        // This.secret should be always true for the first condition, but getType() is overridable
    +        // Here we define an additional check just in case getType() is overridden in another implementation
    +        return secret || calculateIfSecret(target != null ? target.getClass() : null, this.name);
    +    }
     
         public void setValue(Owner target, Type value) throws Exception {
    -        LOGGER.info("Setting " + target + '.' + name + " = " + (getType() == Secret.class ? "****" : value));
    +        LOGGER.info("Setting " + target + '.' + name + " = " + (isSecret(target) ? "****" : value));
             setter.setValue(target, value);
         }
     
    @@ -226,7 +258,6 @@ public boolean equals(Owner o1, Owner o2) throws Exception {
         @FunctionalInterface
         public interface Setter<O,T> {
             void setValue(O target, T value) throws Exception;
    -
             Setter NOP =  (o,v) -> {};
         }
     
    @@ -238,32 +269,103 @@ public interface Getter<O,T> {
             T getValue(O target) throws Exception;
         }
     
    +    @CheckForNull
    +    private static Method locateGetter(Class<?> clazz, @Nonnull String fieldName) {
    +        final String upname = StringUtils.capitalize(fieldName);
    +        final List<String> accessors = Arrays.asList("get" + upname, "is" + upname);
    +
    +        for (Method method : clazz.getMethods()) {
    +            if (method.getParameterCount() != 0) continue;
    +            if (accessors.contains(method.getName())) {
    +                return method;
    +            }
    +
    +            final Exported exported = method.getAnnotation(Exported.class);
    +            if (exported != null && exported.name().equalsIgnoreCase(fieldName)) {
    +                return method;
    +            }
    +        }
    +        return null;
    +    }
    +
    +    @CheckForNull
    +    private static Field locatePublicField(Class<?> clazz, @Nonnull String fieldName) {
    +        return ExtraFieldUtils.getField(clazz, fieldName, false);
    +    }
    +
    +    @CheckForNull
    +    private static Field locatePrivateFieldInHierarchy(Class<?> clazz, @Nonnull String fieldName) {
    +        return ExtraFieldUtils.getFieldNoForce(clazz, fieldName);
    +    }
    +
    +    //TODO: consider Boolean and third condition
    +    /**
    +     * This is a method which tries to guess whether an attribute is {@link Secret}.
    +     * @param targetClass Class of the target object. {@code null} if unknown
    +     * @param fieldName Field name
    +     * @return {@code true} if the attribute is secret
    +     *         {@code false} if not or if there is no conclusive answer.
    +     */
    +    @VisibleForTesting
    +    /*package*/ static boolean calculateIfSecret(@CheckForNull Class<?> targetClass, @Nonnull String fieldName) {
    +        if (targetClass == Secret.class) { // Class is final, so the check is safe
    +            LOGGER.log(Level.FINER, "Attribute {0}#{1} is secret, because it has a Secret type",
    +                    new Object[] {targetClass.getName(), fieldName});
    +            return true;
    +        }
    +
    +        if (targetClass == null) {
    +            LOGGER.log(Level.FINER, "Attribute {0} is assumed to be non-secret, because there is no class instance in the call. " +
    +                            "This call is used only for fast-fetch caching, and the result may be adjusted later",
    +                    new Object[] {fieldName});
    +            return false; // All methods below require a known target class
    +        }
    +
    +        //TODO: Cache decisions?
    +
    +        Method m = locateGetter(targetClass, fieldName);
    +        if (m != null && m.getReturnType() == Secret.class) {
    +            LOGGER.log(Level.FINER, "Attribute {0}#{1} is secret, because there is a getter {2} which returns a Secret type",
    +                    new Object[] {targetClass.getName(), fieldName, m});
    +            return true;
    +        }
    +
    +        Field f = locatePublicField(targetClass, fieldName);
    +        if (f != null && f.getType() == Secret.class) {
    +            LOGGER.log(Level.FINER, "Attribute {0}#{1} is secret, because there is a public field {2} which has a Secret type",
    +                    new Object[] {targetClass.getName(), fieldName, f});
    +            return true;
    +        }
    +
    +        f = locatePrivateFieldInHierarchy(targetClass, fieldName);
    +        if (f != null && f.getType() == Secret.class) {
    +            LOGGER.log(Level.FINER, "Attribute {0}#{1} is secret, because there is a private field {2} which has a Secret type",
    +                    new Object[] {targetClass.getName(), fieldName, f});
    +            return true;
    +        }
    +
    +        // TODO(oleg_nenashev): Consider setters? Gonna be more interesting since there might be many of them
    +        LOGGER.log(Level.FINER, "Attribute {0}#{1} is not a secret, because all checks have passed",
    +                new Object[] {targetClass.getName(), fieldName});
    +        return false;
    +    }
    +
         private Type _getValue(Owner target) throws ConfiguratorException {
    -        try {
    -            final Class<?> clazz = target.getClass();
    -            final String upname = StringUtils.capitalize(name);
    -            final List<String> accessors = Arrays.asList("get" + upname, "is" + upname);
    -
    -            for (Method method : clazz.getMethods()) {
    -                if (method.getParameterCount() != 0) continue;
    -                if (accessors.contains(method.getName())) {
    -                    return (Type) method.invoke(target);
    -                }
    +        final Class<?> clazz = target.getClass();
     
    -                final Exported exported = method.getAnnotation(Exported.class);
    -                if (exported != null && exported.name().equalsIgnoreCase(name)) {
    -                    return (Type) method.invoke(target);
    -                }
    +        try {
    +            final Method method = locateGetter(clazz, this.name);
    +            if (method != null) {
    +                return (Type) method.invoke(target);
                 }
     
                 // If this is a public final field, developers don't define getters as jelly can use them as-is
    -            final Field field = FieldUtils.getField(clazz, name, true);
    -            if (field == null) {
    -                throw new ConfiguratorException("Can't read attribute '" + name + "' from "+ target);
    +            final Field field = ExtraFieldUtils.getField(clazz, this.name, true);
    +            if (field != null) {
    +                return (Type) field.get(target);
                 }
     
    -            return (Type) field.get(target);
    -
    +            throw new ConfiguratorException("Can't read attribute '" + name + "' from "+ target);
             } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
                 throw new ConfiguratorException("Can't read attribute '" + name + "' from "+ target, e);
             }
    
  • plugin/src/main/java/io/jenkins/plugins/casc/util/ExtraFieldUtils.java+99 0 added
    @@ -0,0 +1,99 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +package io.jenkins.plugins.casc.util;
    +
    +import java.lang.reflect.Field;
    +import java.util.Iterator;
    +import javax.annotation.CheckForNull;
    +import org.apache.commons.lang.ClassUtils;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
    +
    +/**
    + * Extends {@link org.apache.commons.lang.reflect.FieldUtils} by adding
    + * some utility methods.
    + */
    +@Restricted(NoExternalUse.class)
    +public class ExtraFieldUtils extends org.apache.commons.lang.reflect.FieldUtils {
    +
    +    /**
    +     * Gets an accessible <code>Field</code> by name without breaking scope.
    +     * Superclasses/interfaces will be considered.
    +     * This is an equivalent of {@link #getField(Class, String, boolean)} from the commons lang library,
    +     * but without {@link Field#setAccessible(boolean)} invocation.
    +     *
    +     * @param cls  the class to reflect, must not be null
    +     * @param fieldName  the field name to obtain
    +     * @return the Field object, it might be {@code null}
    +     * @throws IllegalArgumentException if the class or field name is null
    +     */
    +    @CheckForNull
    +    public static Field getFieldNoForce(final Class cls, String fieldName) {
    +        if (cls == null) {
    +            throw new IllegalArgumentException("The class must not be null");
    +        }
    +        if (fieldName == null) {
    +            throw new IllegalArgumentException("The field name must not be null");
    +        }
    +        // Sun Java 1.3 has a bugged implementation of getField hence we write the
    +        // code ourselves
    +
    +        // getField() will return the Field object with the declaring class
    +        // set correctly to the class that declares the field. Thus requesting the
    +        // field on a subclass will return the field from the superclass.
    +        //
    +        // priority order for lookup:
    +        // searchclass private/protected/package/public
    +        // superclass protected/package/public
    +        //  private/different package blocks access to further superclasses
    +        // implementedinterface public
    +
    +        // check up the superclass hierarchy
    +        for (Class acls = cls; acls != null; acls = acls.getSuperclass()) {
    +            try {
    +                Field field = acls.getDeclaredField(fieldName);
    +                // getDeclaredField checks for non-public scopes, and we return it to upstream
    +                return field;
    +            } catch (NoSuchFieldException ex) {
    +                // ignore
    +            }
    +        }
    +        // check the public interface case. This must be manually searched for
    +        // incase there is a public supersuperclass field hidden by a private/package
    +        // superclass field.
    +        Field match = null;
    +        for (Iterator intf = ClassUtils.getAllInterfaces(cls).iterator(); intf
    +                .hasNext();) {
    +            try {
    +                Field test = ((Class) intf.next()).getField(fieldName);
    +                if (match != null) {
    +                    throw new IllegalArgumentException(
    +                            "Reference to field "
    +                                    + fieldName
    +                                    + " is ambiguous relative to "
    +                                    + cls
    +                                    + "; a matching field exists on two or more implemented interfaces.");
    +                }
    +                match = test;
    +            } catch (NoSuchFieldException ex) {
    +                // ignore
    +            }
    +        }
    +        return match;
    +    }
    +
    +}
    
  • plugin/src/test/java/io/jenkins/plugins/casc/AttributeTest.java+185 0 added
    @@ -0,0 +1,185 @@
    +package io.jenkins.plugins.casc;
    +
    +import hudson.util.Secret;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
    +import org.junit.After;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.jvnet.hudson.test.For;
    +import org.jvnet.hudson.test.Issue;
    +import org.jvnet.hudson.test.LoggerRule;
    +import org.kohsuke.stapler.DataBoundConstructor;
    +
    +import static org.junit.Assert.assertFalse;
    +import static org.junit.Assert.assertTrue;
    +
    +@For(Attribute.class)
    +public class AttributeTest {
    +
    +    @Rule
    +    public LoggerRule logging = new LoggerRule();
    +
    +    @Before
    +    public void tearUp() {
    +        logging.record(Logger.getLogger(Attribute.class.getName()), Level.FINEST).capture(2048);
    +    }
    +
    +    @After
    +    public void tearDown() {
    +        for (String entry : logging.getMessages()) {
    +            System.out.println(entry);
    +        }
    +    }
    +
    +    @Test
    +    @Issue("SECURITY-1279")
    +    public void checkCommonSecretPatterns() {
    +        assertFieldIsSecret(WellDefinedField.class, "secretField");
    +        assertFieldIsSecret(SecretFromGetter.class, "secretField");
    +        assertFieldIsSecret(SecretFromPublicField.class, "secretField");
    +        assertFieldIsSecret(SecretFromPrivateField.class, "secretField");
    +
    +        assertFieldIsSecret(SecretRenamedFieldFithSecretConstructor.class, "mySecretValueField");
    +    }
    +
    +    @Test
    +    @Issue("SECURITY-1279")
    +    public void checkStaticResolution() {
    +        // Field is not secret, but the attribute is secret
    +        assertFieldIsNotSecret(SecretRenamedFieldFithSecretConstructor.class, "mySecretValue");
    +    }
    +
    +    @Test
    +    @Issue("SECURITY-1279")
    +    public void checkCommonSecretPatternsForOverrides() {
    +        assertFieldIsSecret(WellDefinedField2.class, "secret");
    +        assertFieldIsSecret(SecretFromGetter2.class, "secretField");
    +
    +        // Fields
    +        assertFieldIsSecret(SecretFromPublicField2.class, "secretField");
    +        assertFieldIsSecret(SecretFromPrivateField2.class, "secretField");
    +        assertFieldIsSecret(SecretFromPrivateField3.class, "secretField");
    +    }
    +
    +    @Test
    +    @Issue("SECURITY-1279")
    +    public void checkNonSecretPatterns() {
    +        assertFieldIsNotSecret(NonSecretField.class, "passwordPath");
    +    }
    +
    +    public static void assertFieldIsSecret(Class<?> clazz, String fieldName) {
    +        String displayName = clazz != null ? (clazz.getName() + "#" + fieldName) : fieldName;
    +        assertTrue("Field is not secret: " + displayName,
    +                Attribute.calculateIfSecret(clazz, fieldName));
    +    }
    +
    +    public static void assertFieldIsNotSecret(Class<?> clazz, String fieldName) {
    +        String displayName = clazz != null ? (clazz.getName() + "#" + fieldName) : fieldName;
    +        assertFalse("Field is a secret while it should not be considered as one: " + displayName,
    +                Attribute.calculateIfSecret(clazz, fieldName));
    +    }
    +
    +    public static class WellDefinedField {
    +
    +        Secret secretField;
    +
    +        @DataBoundConstructor
    +        public WellDefinedField(Secret secretField) {
    +            this.secretField = secretField;
    +        }
    +
    +        public Secret getSecret() {
    +            return secretField;
    +        }
    +    }
    +
    +    public static class WellDefinedField2 extends WellDefinedField {
    +        public WellDefinedField2(Secret secret) {
    +            super(secret);
    +        }
    +    }
    +
    +    public static class SecretFromGetter {
    +
    +        Secret secretField;
    +
    +        @DataBoundConstructor
    +        public SecretFromGetter(String secretField) {
    +            this.secretField = Secret.fromString(secretField);
    +        }
    +
    +        public Secret getSecret() {
    +            return secretField;
    +        }
    +    }
    +
    +    public static class SecretFromGetter2 extends SecretFromGetter {
    +        public SecretFromGetter2(String secret) {
    +            super(secret);
    +        }
    +    }
    +
    +    public static class SecretFromPublicField {
    +
    +        public Secret secretField;
    +
    +        @DataBoundConstructor
    +        public SecretFromPublicField(String secretField) {
    +            this.secretField = Secret.fromString(secretField);
    +        }
    +    }
    +
    +    public static class SecretFromPublicField2 extends SecretFromPublicField {
    +        public SecretFromPublicField2(String secret) {
    +            super(secret);
    +        }
    +    }
    +
    +    public static class SecretFromPrivateField {
    +
    +        private Secret secretField;
    +
    +        @DataBoundConstructor
    +        public SecretFromPrivateField(String secretField) {
    +            this.secretField = Secret.fromString(secretField);
    +        }
    +    }
    +
    +    public static class SecretFromPrivateField2 extends SecretFromPrivateField {
    +        public SecretFromPrivateField2(String secret) {
    +            super(secret);
    +        }
    +    }
    +
    +    public static class SecretFromPrivateField3 extends SecretFromPrivateField2 {
    +        public SecretFromPrivateField3(String secret) {
    +            super(secret);
    +        }
    +    }
    +
    +    public static class NonSecretField {
    +
    +        public String passwordPath;
    +
    +        @DataBoundConstructor
    +        public NonSecretField(String passwordPath) {
    +            this.passwordPath = passwordPath;
    +        }
    +    }
    +
    +    public static class SecretRenamedFieldFithSecretConstructor {
    +
    +        Secret mySecretValueField;
    +
    +        @DataBoundConstructor
    +        public SecretRenamedFieldFithSecretConstructor(Secret mySecretValue) {
    +            this.mySecretValueField = mySecretValue;
    +        }
    +
    +        public String getMySecretValue() {
    +            return mySecretValueField.getPlainText();
    +        }
    +    }
    +}
    
  • plugin/src/test/java/io/jenkins/plugins/casc/core/ProxyConfiguratorTest.java+27 6 modified
    @@ -2,21 +2,25 @@
     
     import hudson.ProxyConfiguration;
     import hudson.util.Secret;
    +import io.jenkins.plugins.casc.Attribute;
     import io.jenkins.plugins.casc.ConfigurationContext;
     import io.jenkins.plugins.casc.Configurator;
     import io.jenkins.plugins.casc.ConfiguratorRegistry;
     import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
     import io.jenkins.plugins.casc.misc.Env;
     import io.jenkins.plugins.casc.misc.EnvVarsRule;
     import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
    +import io.jenkins.plugins.casc.misc.Util;
     import io.jenkins.plugins.casc.model.CNode;
     import io.jenkins.plugins.casc.model.Mapping;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
     import org.junit.Rule;
     import org.junit.Test;
     import org.junit.rules.RuleChain;
    +import org.jvnet.hudson.test.Issue;
    +import org.jvnet.hudson.test.LoggerRule;
     
    -import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot;
    -import static io.jenkins.plugins.casc.misc.Util.toYamlString;
     import static java.util.Objects.requireNonNull;
     import static org.junit.Assert.assertEquals;
     import static org.junit.Assert.assertNotNull;
    @@ -25,10 +29,12 @@
     public class ProxyConfiguratorTest {
     
         final JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule();
    +    public LoggerRule logging = new LoggerRule();
     
         @Rule
         public RuleChain chain = RuleChain
    -            .outerRule(new EnvVarsRule())
    +            .outerRule(logging.record(Logger.getLogger(Attribute.class.getName()), Level.INFO).capture(2048))
    +            .around(new EnvVarsRule())
                 .around(j);
     
         @Test
    @@ -93,6 +99,21 @@ public void shouldSetProxyWithSecretInFields() {
             assertEquals(proxy.getTestUrl(), "http://google.com");
         }
     
    +    @Test
    +    @Env(name = "PROXY_USER", value = "proxy_user")
    +    @Env(name = "PROXY_PASSWORD", value = "proxy_password")
    +    @ConfiguredWithCode("ProxyWithSecrets.yml")
    +    @Issue("SECURITY-1303") // Fixed in 1.20
    +    public void shouldNotWritePasswordToLog() {
    +        ProxyConfiguration proxy = j.jenkins.proxy;
    +        assertEquals(proxy.getUserName(), "proxy_user");
    +        assertEquals(Secret.decrypt(proxy.getEncryptedPassword()).getPlainText(), "proxy_password");
    +
    +        // Check logs
    +        Util.assertLogContains(logging, "password");
    +        Util.assertNotInLog(logging, "proxy_password");
    +    }
    +
         @Test
         @ConfiguredWithCode("Proxy.yml")
         public void describeProxyConfig() throws Exception {
    @@ -102,7 +123,7 @@ public void describeProxyConfig() throws Exception {
     
             Secret password = requireNonNull(Secret.decrypt(getProxyNode(context).getScalarValue("password")));
     
    -        final String yamlConfig = toYamlString(configNode);
    +        final String yamlConfig = Util.toYamlString(configNode);
             assertEquals(String.join("\n",
                     "name: \"proxyhost\"",
                     "noProxyHost: \"externalhost\"",
    @@ -123,7 +144,7 @@ public void describeMinimalProxyConfig() throws Exception {
     
             Secret password = requireNonNull(Secret.decrypt(getProxyNode(context).getScalarValue("password")));
     
    -        final String yamlConfig = toYamlString(configNode);
    +        final String yamlConfig = Util.toYamlString(configNode);
             assertEquals(String.join("\n",
                     "name: \"proxyhost\"",
                     "password: \"" + password.getEncryptedValue() + "\"", // It's an empty string here
    @@ -133,6 +154,6 @@ public void describeMinimalProxyConfig() throws Exception {
         }
     
         private Mapping getProxyNode(ConfigurationContext context) throws Exception {
    -        return getJenkinsRoot(context).get("proxy").asMapping();
    +        return Util.getJenkinsRoot(context).get("proxy").asMapping();
         }
     }
    
  • plugin/src/test/java/io/jenkins/plugins/casc/misc/JenkinsConfiguredWithCodeRule.java+21 0 modified
    @@ -1,7 +1,9 @@
     package io.jenkins.plugins.casc.misc;
     
     import io.jenkins.plugins.casc.ConfigurationAsCode;
    +import java.io.ByteArrayOutputStream;
     import java.lang.reflect.Field;
    +import java.nio.charset.StandardCharsets;
     import java.util.Arrays;
     import java.util.List;
     import java.util.Objects;
    @@ -76,4 +78,23 @@ private ConfiguredWithCode getConfiguredWithCode() {
             }
             return null;
         }
    +
    +    //TODO: Looks like API defect, exception should be thrown
    +    /**
    +     * Exports the Jenkins configuration to a string.
    +     * @return YAML as string
    +     * @param strict Fail if any export operation returns error
    +     * @throws Exception Export error
    +     * @throws AssertionError Failed to export the configuration
    +     * @since TODO
    +     */
    +    public String exportToString(boolean strict) throws Exception {
    +        final ByteArrayOutputStream out = new ByteArrayOutputStream();
    +        ConfigurationAsCode.get().export(out);
    +        final String s = out.toString(StandardCharsets.UTF_8.name());
    +        if (strict && s.contains("Failed to export")) {
    +            throw new AssertionError("Failed to export the configuration: " + s);
    +        }
    +        return s;
    +    }
     }
    
  • plugin/src/test/java/io/jenkins/plugins/casc/misc/Util.java+25 0 modified
    @@ -19,8 +19,11 @@
     import java.util.Objects;
     import jenkins.model.GlobalConfigurationCategory;
     import jenkins.tools.ToolConfigurationCategory;
    +import org.jvnet.hudson.test.LoggerRule;
     
     import static io.jenkins.plugins.casc.ConfigurationAsCode.serializeYamlNode;
    +import static org.junit.Assert.assertFalse;
    +import static org.junit.Assert.assertTrue;
     
     public class Util {
     
    @@ -158,4 +161,26 @@ public static String toStringFromYamlFile(Object clazz, String resourcePath) thr
             byte[] bytes = Files.readAllBytes(Paths.get(resource.toURI()));
             return new String(bytes, StandardCharsets.UTF_8).replaceAll("\r\n?", "\n");
         }
    +
    +    /**
    +     * Checks whether {@link LoggerRule} has not recorded the message.
    +     * @param logging Logger rule
    +     * @param unexpectedText Text to check
    +     * @since TODO
    +     */
    +    public static void assertNotInLog(LoggerRule logging, String unexpectedText) {
    +        assertFalse("The log should not contain '" + unexpectedText + "'",
    +                logging.getMessages().stream().anyMatch(m -> m.contains(unexpectedText)));
    +    }
    +
    +    /**
    +     * Checks whether {@link LoggerRule} has recorded the message.
    +     * @param logging Logger rule
    +     * @param expectedText Text to check
    +     * @since TODO
    +     */
    +    public static void assertLogContains(LoggerRule logging, String expectedText) {
    +        assertTrue("The log should contain '" + expectedText + "'",
    +                logging.getMessages().stream().anyMatch(m -> m.contains(expectedText)));
    +    }
     }
    
  • plugin/src/test/java/io/jenkins/plugins/casc/yaml/YamlExportTest.java+40 0 added
    @@ -0,0 +1,40 @@
    +package io.jenkins.plugins.casc.yaml;
    +
    +import io.jenkins.plugins.casc.Attribute;
    +import io.jenkins.plugins.casc.AttributeTest;
    +import io.jenkins.plugins.casc.ConfigurationAsCode;
    +import io.jenkins.plugins.casc.ConfigurationContext;
    +import io.jenkins.plugins.casc.ConfiguratorRegistry;
    +import io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator;
    +import java.util.Set;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.jvnet.hudson.test.Issue;
    +import org.jvnet.hudson.test.JenkinsRule;
    +
    +import static org.hamcrest.CoreMatchers.equalTo;
    +import static org.hamcrest.MatcherAssert.assertThat;
    +import static org.junit.Assert.assertTrue;
    +
    +/**
    + * Contains tests for particular export cases.
    + */
    +public class YamlExportTest {
    +
    +    @Rule
    +    public JenkinsRule j = new JenkinsRule();
    +
    +    @Test
    +    @Issue("SECURITY-1458")
    +    public void shouldDiscoverSecretsBasedOnTheAttributeType() throws Exception {
    +        ConfigurationAsCode casc = ConfigurationAsCode.get();
    +        ConfiguratorRegistry registry = ConfiguratorRegistry.get();
    +        ConfigurationContext context = new ConfigurationContext(registry);
    +
    +        DataBoundConfigurator c = new DataBoundConfigurator<>(AttributeTest.SecretRenamedFieldFithSecretConstructor.class);
    +        Set<Attribute> attributes = c.describe();
    +        assertThat(attributes.size(), equalTo(1));
    +        Attribute attr = attributes.iterator().next();
    +        assertTrue(attr.isSecret(null));
    +    }
    +}
    

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

0

No linked articles in our index yet.