Skip to content

Commit

Permalink
SECURITY-3373
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-beck authored and Kevin-CB committed Sep 23, 2024
1 parent 8ff5df2 commit 4ebfab7
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 1 deletion.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<properties>
<changelist>999999-SNAPSHOT</changelist>
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>
<jenkins.version>2.426.3</jenkins.version>
<jenkins.version>2.462.3</jenkins.version>
</properties>

<repositories>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.cloudbees.plugins.credentials;

import hudson.ExtensionList;
import hudson.init.Initializer;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.security.ExtendedReadRedaction;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

@Restricted(NoExternalUse.class)
public class SecretBytesReactionExtension {

public static final Logger LOGGER = Logger.getLogger(SecretBytesReactionExtension.class.getName());

// TODO Delete this and annotate `SecretBytesRedaction` with `@Extension` once the core dependency is >= 2.479
@Initializer
public static void create() {
try {
ExtensionList.lookup(ExtendedReadRedaction.class).add(new SecretBytesRedaction());
} catch (NoClassDefFoundError unused) {
LOGGER.log(Level.WARNING, "Failed to register SecretBytesRedaction. Update Jenkins to add support for redacting credentials in config.xml files from users with ExtendedRead permission. Learn more: https://www.jenkins.io/redirect/plugin/credentials/SecretBytesRedaction/");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.cloudbees.plugins.credentials;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jenkins.security.ExtendedReadRedaction;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

@Restricted(NoExternalUse.class)
// @Extension
// See SecretBytesReactionExtension
public class SecretBytesRedaction implements ExtendedReadRedaction {
private static final Pattern SECRET_BYTES_PATTERN = Pattern.compile(">(" + SecretBytes.ENCRYPTED_VALUE_PATTERN + ")<");

@Override
public String apply(String configDotXml) {
Matcher matcher = SECRET_BYTES_PATTERN.matcher(configDotXml);
StringBuilder cleanXml = new StringBuilder();
while (matcher.find()) {
if (SecretBytes.isSecretBytes(matcher.group(1))) {
matcher.appendReplacement(cleanXml, ">********<");
}
}
matcher.appendTail(cleanXml);
return cleanXml.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.cloudbees.plugins.credentials;

import com.cloudbees.hudson.plugins.folder.Folder;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
import hudson.model.Item;
import hudson.model.ModelObject;
import java.util.Base64;
import java.util.Iterator;
import jenkins.model.Jenkins;
import org.htmlunit.Page;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class SecretBytesRedactionTest {
@Rule
public JenkinsRule j = new JenkinsRule();

@Test
public void testRedaction() throws Exception {
final String usernamePasswordPassword = "thisisthe_theuserpassword";
final SecretBytes secretBytes = SecretBytes.fromString("thisis_theTestData");

Item.EXTENDED_READ.setEnabled(true);

final Folder folder = j.jenkins.createProject(Folder.class, "F");
final CredentialsStore store = lookupStore(folder);
final UsernamePasswordCredentialsImpl usernamePasswordCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "passwordid", null, "theusername", usernamePasswordPassword);
store.addCredentials(Domain.global(), usernamePasswordCredentials);
store.addCredentials(Domain.global(), new SecretBytesCredential(CredentialsScope.GLOBAL, "certid", "thedesc", secretBytes));

j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Item.READ, Item.EXTENDED_READ, Jenkins.READ).everywhere().to("bob"));

try (JenkinsRule.WebClient webClient = j.createWebClient().login("alice")) {
final Page page = webClient.goTo("job/F/config.xml", "application/xml");
final String content = page.getWebResponse().getContentAsString();
assertThat(content, containsString(usernamePasswordCredentials.getPassword().getEncryptedValue()));
assertThat(content, containsString(Base64.getEncoder().encodeToString(secretBytes.getEncryptedData())));
}
try (JenkinsRule.WebClient webClient = j.createWebClient().login("bob")) {
final Page page = webClient.goTo("job/F/config.xml", "application/xml");
final String content = page.getWebResponse().getContentAsString();
assertThat(content, not(containsString(usernamePasswordCredentials.getPassword().getEncryptedValue())));
assertThat(content, not(containsString(Base64.getEncoder().encodeToString(secretBytes.getEncryptedData()))));
assertThat(content, containsString("<password>********</password>"));
assertThat(content, containsString("<mySecretBytes>********</mySecretBytes>"));
}
}

// Stolen from BaseStandardCredentialsTest
private static CredentialsStore lookupStore(ModelObject object) {
Iterator<CredentialsStore> stores = CredentialsProvider.lookupStores(object).iterator();
assertTrue(stores.hasNext());
CredentialsStore store = stores.next();
assertEquals("we got the expected store", object, store.getContext());
return store;
}

// This would be nicer with a real credential like `FileCredentialsImpl` but another test falls over if we add `plain-credentials` to the test scope
public static class SecretBytesCredential extends BaseStandardCredentials {
private final SecretBytes mySecretBytes;

public SecretBytesCredential(CredentialsScope scope, String id, String description, SecretBytes bytes) {
super(scope, id, description);
this.mySecretBytes = bytes;
}
}
}

0 comments on commit 4ebfab7

Please sign in to comment.