Security context
Critical· 9.8GHSA-92mr-4w2q-4578 CVE-2017-1000362CWE-200Published May 17, 2022

Exposure of Sensitive Information to an Unauthorized Actor in Jenkins

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

1.498 → fixed in 2.32.22.40 → fixed in 2.44

Details

The re-key admin monitor was introduced in Jenkins 1.498 and re-encrypted all secrets in JENKINS_HOME with a new key. It also created a backup directory with all old secrets, and the key used to encrypt them. These backups were world-readable and not removed afterwards. Jenkins now deletes the backup directory, if present. Upgrading from before 1.498 will no longer create a backup directory. Administrators relying on file access permissions in their manually created backups are advised to check them for the directory $JENKINS_HOME/jenkins.security.RekeySecretAdminMonitor/backups, and delete it if present.

The fix

[SECURITY-376] Remove backup directory for

Jesse Glick· Dec 21, 2016, 09:21 PM+2027a572450f03
core/src/main/java/hudson/util/SecretRewriter.java+15 23
@@ -2,7 +2,6 @@
import com.trilead.ssh2.crypto.Base64;
import hudson.model.TaskListener;
-import org.apache.commons.io.FileUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
@@ -33,21 +32,21 @@ public class SecretRewriter {
*/
private int count;
- /**
- * If non-null the original file before rewrite gets in here.
- */
- private final File backupDirectory;
-
/**
* Canonical paths of the directories we are recursing to protect
* against symlink induced cycles.
*/
private Set<String> callstack = new HashSet<String>();
- public SecretRewriter(File backupDirectory) throws GeneralSecurityException {
+ public SecretRewriter() throws GeneralSecurityException {
cipher = Secret.getCipher("AES");
key = Secret.getLegacyKey();
- this.backupDirectory = backupDirectory;
+ }
+
+ /** @deprecated SECURITY-376: {@code backupDirectory} is ignored */
+ @Deprecated
+ public SecretRewriter(File backupDirectory) throws GeneralSecurityException {
+ this();
}
private String tryRewrite(String s) throws IOException, InvalidKeyException {
@@ -70,12 +69,14 @@ private String tryRewrite(String s) throws IOException, InvalidKeyException {
return s;
}
- /**
- * @param backup
- * if non-null, the original file will be copied here before rewriting.
- * if the rewrite doesn't happen, no copying.
- */
+ /** @deprecated SECURITY-376: {@code backup} is ignored */
+ @Deprecated
public boolean rewrite(File f, File backup) throws InvalidKeyException, IOException {
+ return rewrite(f);
+ }
+
+ public boolean rewrite(File f) throws InvalidKeyException, IOException {
+
AtomicFileWriter w = new AtomicFileWriter(f, "UTF-8");
try {
PrintWriter out = new PrintWriter(new BufferedWriter(w));
@@ -117,10 +118,6 @@ public boolean rewrite(File f, File backup) throws InvalidKeyException, IOExcept
}
if (modified) {
- if (backup!=null) {
- backup.getParentFile().mkdirs();
- FileUtils.copyFile(f,backup);
- }
w.commit();
}
return modified;
@@ -165,11 +162,7 @@ private int rewriteRecursive(File dir, String relative, TaskListener listener) t
if ((count++)%100==0)
listener.getLogger().println("Scanning "+child);
try {
- File backup = null;
- if (backupDirectory!=null) backup = new File(backupDirectory,relative+'/'+ cn);
- if (rewrite(child,backup)) {
- if (backup!=null)
- listener.getLogger().println("Copied "+child+" to "+backup+" as a backup");
+ if (rewrite(child)) {
listener.getLogger().println("Rewritten "+child);
rewritten++;
}
@@ -199,7 +192,6 @@ protected boolean isIgnoredDir(File dir) {
String n = dir.getName();
return n.equals("workspace") || n.equals("artifacts")
|| n.equals("plugins") // no mutable data here
- || n.equals("jenkins.security.RekeySecretAdminMonitor") // we don't want to rewrite backups
|| n.equals(".") || n.equals("..");
}
core/src/main/java/jenkins/security/RekeySecretAdminMonitor.java+4 1
@@ -1,6 +1,7 @@
package jenkins.security;
import hudson.Extension;
+import hudson.Util;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.TaskListener;
@@ -50,6 +51,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor {
*/
private final FileBoolean scanOnBoot = state("scanOnBoot");
+ @SuppressWarnings("OverridableMethodCallInConstructor") // should have been final
public RekeySecretAdminMonitor() throws IOException {
// if JENKINS_HOME existed <1.497, we need to offer rewrite
// this computation needs to be done and the value be captured,
@@ -59,6 +61,7 @@ public RekeySecretAdminMonitor() throws IOException {
if (j.isUpgradedFromBefore(new VersionNumber("1.496.*"))
&& new FileBoolean(new File(j.getRootDir(),"secret.key.not-so-secret")).isOff())
needed.on();
+ Util.deleteRecursive(new File(getBaseDir(), "backups")); // SECURITY-376: no longer used
}
@Override
@@ -133,7 +136,7 @@ protected File getLogFile() {
protected void fix(TaskListener listener) throws Exception {
LOGGER.info("Initiating a re-keying of secrets. See "+getLogFile());
- SecretRewriter rewriter = new SecretRewriter(new File(getBaseDir(),"backups"));
+ SecretRewriter rewriter = new SecretRewriter();
try {
PrintStream log = listener.getLogger();
core/src/test/groovy/hudson/util/SecretRewriterTest.groovy+1 3
@@ -70,8 +70,7 @@ class SecretRewriterTest {
*/
@Test
void recursionDetection() {
- def backup = tmp.newFolder("backup")
- def sw = new SecretRewriter(backup);
+ def sw = new SecretRewriter();
def st = StreamTaskListener.fromStdout()
def o = encryptOld("Hello world")
@@ -101,7 +100,6 @@ class SecretRewriterTest {
dirs.each { p->
assert new File(t,"$p/foo.xml").text.trim()==answer
- assert new File(backup,"$p/foo.xml").text.trim()==payload
}
// t2 is only reachable by following a symlink. this should be covered, too

[SECURITY-376] Remove backup directory for

Jesse Glick· Dec 21, 2016, 09:21 PM+2027a572450f03
core/src/main/java/hudson/util/SecretRewriter.java+15 23
@@ -2,7 +2,6 @@
import com.trilead.ssh2.crypto.Base64;
import hudson.model.TaskListener;
-import org.apache.commons.io.FileUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
@@ -33,21 +32,21 @@ public class SecretRewriter {
*/
private int count;
- /**
- * If non-null the original file before rewrite gets in here.
- */
- private final File backupDirectory;
-
/**
* Canonical paths of the directories we are recursing to protect
* against symlink induced cycles.
*/
private Set<String> callstack = new HashSet<String>();
- public SecretRewriter(File backupDirectory) throws GeneralSecurityException {
+ public SecretRewriter() throws GeneralSecurityException {
cipher = Secret.getCipher("AES");
key = Secret.getLegacyKey();
- this.backupDirectory = backupDirectory;
+ }
+
+ /** @deprecated SECURITY-376: {@code backupDirectory} is ignored */
+ @Deprecated
+ public SecretRewriter(File backupDirectory) throws GeneralSecurityException {
+ this();
}
private String tryRewrite(String s) throws IOException, InvalidKeyException {
@@ -70,12 +69,14 @@ private String tryRewrite(String s) throws IOException, InvalidKeyException {
return s;
}
- /**
- * @param backup
- * if non-null, the original file will be copied here before rewriting.
- * if the rewrite doesn't happen, no copying.
- */
+ /** @deprecated SECURITY-376: {@code backup} is ignored */
+ @Deprecated
public boolean rewrite(File f, File backup) throws InvalidKeyException, IOException {
+ return rewrite(f);
+ }
+
+ public boolean rewrite(File f) throws InvalidKeyException, IOException {
+
AtomicFileWriter w = new AtomicFileWriter(f, "UTF-8");
try {
PrintWriter out = new PrintWriter(new BufferedWriter(w));
@@ -117,10 +118,6 @@ public boolean rewrite(File f, File backup) throws InvalidKeyException, IOExcept
}
if (modified) {
- if (backup!=null) {
- backup.getParentFile().mkdirs();
- FileUtils.copyFile(f,backup);
- }
w.commit();
}
return modified;
@@ -165,11 +162,7 @@ private int rewriteRecursive(File dir, String relative, TaskListener listener) t
if ((count++)%100==0)
listener.getLogger().println("Scanning "+child);
try {
- File backup = null;
- if (backupDirectory!=null) backup = new File(backupDirectory,relative+'/'+ cn);
- if (rewrite(child,backup)) {
- if (backup!=null)
- listener.getLogger().println("Copied "+child+" to "+backup+" as a backup");
+ if (rewrite(child)) {
listener.getLogger().println("Rewritten "+child);
rewritten++;
}
@@ -199,7 +192,6 @@ protected boolean isIgnoredDir(File dir) {
String n = dir.getName();
return n.equals("workspace") || n.equals("artifacts")
|| n.equals("plugins") // no mutable data here
- || n.equals("jenkins.security.RekeySecretAdminMonitor") // we don't want to rewrite backups
|| n.equals(".") || n.equals("..");
}
core/src/main/java/jenkins/security/RekeySecretAdminMonitor.java+4 1
@@ -1,6 +1,7 @@
package jenkins.security;
import hudson.Extension;
+import hudson.Util;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.TaskListener;
@@ -50,6 +51,7 @@ public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor {
*/
private final FileBoolean scanOnBoot = state("scanOnBoot");
+ @SuppressWarnings("OverridableMethodCallInConstructor") // should have been final
public RekeySecretAdminMonitor() throws IOException {
// if JENKINS_HOME existed <1.497, we need to offer rewrite
// this computation needs to be done and the value be captured,
@@ -59,6 +61,7 @@ public RekeySecretAdminMonitor() throws IOException {
if (j.isUpgradedFromBefore(new VersionNumber("1.496.*"))
&& new FileBoolean(new File(j.getRootDir(),"secret.key.not-so-secret")).isOff())
needed.on();
+ Util.deleteRecursive(new File(getBaseDir(), "backups")); // SECURITY-376: no longer used
}
@Override
@@ -133,7 +136,7 @@ protected File getLogFile() {
protected void fix(TaskListener listener) throws Exception {
LOGGER.info("Initiating a re-keying of secrets. See "+getLogFile());
- SecretRewriter rewriter = new SecretRewriter(new File(getBaseDir(),"backups"));
+ SecretRewriter rewriter = new SecretRewriter();
try {
PrintStream log = listener.getLogger();
core/src/test/groovy/hudson/util/SecretRewriterTest.groovy+1 3
@@ -70,8 +70,7 @@ class SecretRewriterTest {
*/
@Test
void recursionDetection() {
- def backup = tmp.newFolder("backup")
- def sw = new SecretRewriter(backup);
+ def sw = new SecretRewriter();
def st = StreamTaskListener.fromStdout()
def o = encryptOld("Hello world")
@@ -101,7 +100,6 @@ class SecretRewriterTest {
dirs.each { p->
assert new File(t,"$p/foo.xml").text.trim()==answer
- assert new File(backup,"$p/foo.xml").text.trim()==payload
}
// t2 is only reachable by following a symlink. this should be covered, too

References