Skip to content

Commit c6d20da

Browse files
committed
SECURITY-2186
1 parent 8f088ce commit c6d20da

18 files changed

+582
-14
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@
216216
<systemPropertyVariables>
217217
<hudson.remoting.ExportTable.exportTraces>true</hudson.remoting.ExportTable.exportTraces>
218218
</systemPropertyVariables>
219+
<!-- Security2186Test-->
220+
<environmentVariables>
221+
<test2186.trustStorePassword>agsdfaksdgFASKD</test2186.trustStorePassword>
222+
</environmentVariables>
223+
<argLine>-Dtest2186.trustStorePassword=ASJDHFGASLDFG</argLine>
224+
219225
</configuration>
220226
</plugin>
221227
</plugins>

src/main/java/com/cloudbees/jenkins/support/api/BaseFileContent.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
import java.io.PrintWriter;
4646
import java.nio.file.Files;
4747
import java.nio.file.NoSuchFileException;
48+
import java.util.function.Function;
4849
import java.util.function.Supplier;
50+
import java.util.function.UnaryOperator;
4951

5052
/**
5153
* Utility class with the common logic to FileContent and UnfilteredFileContent.
@@ -56,18 +58,37 @@
5658
class BaseFileContent {
5759
private final File file;
5860
private final Supplier<InputStream> inputStreamSupplier;
61+
62+
/**
63+
* the function to filter secret text if comes from {@link com.cloudbees.jenkins.support.configfiles.XmlRedactedSecretFileContent} or {@link com.cloudbees.jenkins.support.api.LaunchLogsFileContent}
64+
*/
65+
private final Function<String, String> secretsFilterFunction;
5966
private final long maxSize;
6067
private final boolean isBinary;
6168

6269
private final static String ENCODING = "UTF-8";
6370

71+
/**
72+
* @deprecated (as it is placed in the api package we keep backward compatibility, no relevant usage was found)
73+
*/
74+
@Deprecated
6475
protected BaseFileContent(File file, Supplier<InputStream> inputStreamSupplier) {
65-
this(file, inputStreamSupplier, -1);
76+
this(file, inputStreamSupplier, -1, s -> s);
6677
}
6778

79+
protected BaseFileContent(File file, Supplier<InputStream> inputStreamSupplier, UnaryOperator<String> secretsFilterFunction) {
80+
this(file, inputStreamSupplier, -1, secretsFilterFunction);
81+
}
82+
83+
@Deprecated
6884
protected BaseFileContent(File file, Supplier<InputStream> inputStreamSupplier, long maxSize) {
85+
this(file, inputStreamSupplier, maxSize, s -> s);
86+
}
87+
88+
protected BaseFileContent(File file, Supplier<InputStream> inputStreamSupplier, long maxSize, UnaryOperator<String>secretsFilterFunction) {
6989
this.file = file;
7090
this.inputStreamSupplier = inputStreamSupplier;
91+
this.secretsFilterFunction = secretsFilterFunction;
7192
this.maxSize = maxSize;
7293
this.isBinary = isBinary();
7394
}
@@ -106,7 +127,7 @@ protected void writeTo(OutputStream os, ContentFilter filter) throws IOException
106127
try {
107128
if (maxSize == -1) {
108129
for (String s : Files.readAllLines(file.toPath())) {
109-
String filtered = ContentFilter.filter(filter, s);
130+
String filtered = ContentFilter.filter(filter, secretsFilterFunction.apply(s));
110131
IOUtils.write(filtered, os, ENCODING);
111132
// The new line
112133
IOUtils.write("\n", os, ENCODING);
@@ -115,7 +136,7 @@ protected void writeTo(OutputStream os, ContentFilter filter) throws IOException
115136
try (TruncatedFileReader reader = new TruncatedFileReader(file, maxSize)) {
116137
String s;
117138
while ((s = reader.readLine()) != null) {
118-
String filtered = ContentFilter.filter(filter, s);
139+
String filtered = ContentFilter.filter(filter, secretsFilterFunction.apply(s));
119140
IOUtils.write(filtered, os, ENCODING);
120141
// The new line
121142
IOUtils.write("\n", os, ENCODING);

src/main/java/com/cloudbees/jenkins/support/api/FileContent.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ protected InputStream getInputStream() throws IOException {
9191
return new FileInputStream(file);
9292
}
9393

94+
protected String getSimpleValueOrRedactedPassword(String value) {
95+
return value;
96+
}
97+
9498
private BaseFileContent createBaseFileContent(File file, long maxSize) {
9599
Supplier<InputStream> supplier = () -> {
96100
try {
@@ -99,6 +103,6 @@ private BaseFileContent createBaseFileContent(File file, long maxSize) {
99103
return new ByteArrayInputStream(Functions.printThrowable(e).getBytes(StandardCharsets.UTF_8));
100104
}
101105
};
102-
return new BaseFileContent(file, supplier, maxSize);
106+
return new BaseFileContent(file, supplier, maxSize, this::getSimpleValueOrRedactedPassword);
103107
}
104108
}

src/main/java/com/cloudbees/jenkins/support/api/FilePathContent.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,22 @@
2424

2525
package com.cloudbees.jenkins.support.api;
2626

27+
import com.cloudbees.jenkins.support.filter.FilteredOutputStream;
28+
import com.cloudbees.jenkins.support.filter.PasswordRedactor;
2729
import hudson.FilePath;
2830
import hudson.Functions;
31+
import org.apache.commons.io.IOUtils;
2932

33+
import java.io.BufferedReader;
3034
import java.io.FileNotFoundException;
3135
import java.io.IOException;
36+
import java.io.InputStreamReader;
3237
import java.io.OutputStream;
3338
import java.io.OutputStreamWriter;
3439
import java.io.PrintWriter;
40+
import java.nio.charset.CharsetDecoder;
41+
import java.nio.charset.CodingErrorAction;
42+
import java.nio.charset.StandardCharsets;
3543
import java.nio.file.NoSuchFileException;
3644

3745
/**
@@ -60,7 +68,11 @@ public FilePathContent(String name, String[] filterableParameters, FilePath file
6068
@Override
6169
public void writeTo(OutputStream os) throws IOException {
6270
try {
63-
file.copyTo(os);
71+
if (PasswordRedactor.FILES_WITH_SECRETS.contains(file.getName())) {
72+
copyRedacted(os);
73+
} else {
74+
file.copyTo(os);
75+
}
6476
} catch (InterruptedException e) {
6577
throw new IOException(e);
6678
} catch (IOException e) {
@@ -92,4 +104,18 @@ public long getTime() throws IOException {
92104
throw new IOException(e);
93105
}
94106
}
107+
108+
private void copyRedacted(OutputStream os) throws IOException, InterruptedException {
109+
CharsetDecoder charsetDecoder = StandardCharsets.UTF_8.newDecoder()
110+
.onMalformedInput(CodingErrorAction.REPLACE)
111+
.onUnmappableCharacter(CodingErrorAction.REPLACE)
112+
.replaceWith(FilteredOutputStream.UNKNOWN_INPUT);
113+
114+
try (BufferedReader br = new BufferedReader(new InputStreamReader(file.read(), charsetDecoder))) {
115+
String line;
116+
while ((line = br.readLine()) != null) {
117+
IOUtils.write(PasswordRedactor.get().redact(line), os, charsetDecoder.charset());
118+
}
119+
}
120+
}
95121
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.cloudbees.jenkins.support.api;
2+
3+
import com.cloudbees.jenkins.support.filter.PasswordRedactor;
4+
import org.apache.commons.io.IOUtils;
5+
import java.io.ByteArrayInputStream;
6+
import java.io.File;
7+
import java.io.FileInputStream;
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.nio.charset.Charset;
11+
import java.util.List;
12+
import java.util.stream.Collectors;
13+
14+
public class LaunchLogsFileContent extends FileContent {
15+
16+
public LaunchLogsFileContent(String name, String[] filterableParameters, File file, long maxSize) {
17+
super(name, filterableParameters, file, maxSize);
18+
}
19+
20+
@Override
21+
protected InputStream getInputStream() throws IOException {
22+
try(FileInputStream inputStream = new FileInputStream(file)) {
23+
List<String> strings = IOUtils.readLines(inputStream, Charset.defaultCharset());
24+
byte[] bytes = strings.stream()
25+
.map(line -> PasswordRedactor.get().redact(line))
26+
.collect(Collectors.joining("\n", "", "\n"))
27+
.getBytes(Charset.defaultCharset());
28+
return new ByteArrayInputStream(bytes);
29+
}
30+
}
31+
32+
@Override
33+
protected String getSimpleValueOrRedactedPassword(String value) {
34+
return PasswordRedactor.get().redact(value);
35+
}
36+
}

src/main/java/com/cloudbees/jenkins/support/api/UnfilteredFileContent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private BaseFileContent createBaseFileContent(File file, long maxSize) {
9494
return null;
9595
}
9696
};
97-
return new BaseFileContent(file, supplier, maxSize);
97+
return new BaseFileContent(file, supplier, maxSize, s -> s);
9898
}
9999

100100
@Override

src/main/java/com/cloudbees/jenkins/support/configfiles/SecretHandler.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.cloudbees.jenkins.support.configfiles;
22

3+
import com.cloudbees.jenkins.support.filter.PasswordRedactor;
34
import com.cloudbees.plugins.credentials.SecretBytes;
45
import hudson.util.Secret;
56
import org.apache.commons.io.FileUtils;
@@ -63,6 +64,7 @@ public static String findSecrets(File xmlFile) throws SAXException, IOException,
6364

6465
XMLReader xr = new XMLFilterImpl(XMLReaderFactory.createXMLReader()) {
6566
private String tagName = "";
67+
private String previousStringTagValue;
6668

6769
@Override
6870
public void startElement(String uri, String localName, String qName, Attributes atts)
@@ -87,6 +89,21 @@ public void characters(char[] ch, int start, int length) throws SAXException {
8789
ch = SECRET_MARKER.toCharArray();
8890
start = 0;
8991
length = ch.length;
92+
} else if (isJvmArgsWithSecrets(tagName, value)) {
93+
ch = PasswordRedactor.get().redact(value).toCharArray();
94+
start = 0;
95+
length = ch.length;
96+
} else if ("string".equals(tagName)) {
97+
if (previousStringTagValue != null) {
98+
if (PasswordRedactor.get().match(previousStringTagValue)) {
99+
ch = PasswordRedactor.REDACTED.toCharArray();
100+
start = 0;
101+
length = ch.length;
102+
}
103+
previousStringTagValue = null;
104+
} else {
105+
previousStringTagValue = value;
106+
}
90107
}
91108
}
92109
}
@@ -153,4 +170,8 @@ private static Source createSafeSource(XMLReader reader, InputSource source) {
153170
return new SAXSource(reader, source);
154171
}
155172

173+
private static boolean isJvmArgsWithSecrets(String tagName, String value) {
174+
return ("jvmOptions".equals(tagName) || "vmargs".equals(tagName) || "cmd".equals(tagName)) && PasswordRedactor.get().match(value);
175+
}
176+
156177
}

src/main/java/com/cloudbees/jenkins/support/configfiles/XmlRedactedSecretFileContent.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.cloudbees.jenkins.support.configfiles;
22

33
import com.cloudbees.jenkins.support.api.FileContent;
4+
import com.cloudbees.jenkins.support.filter.PasswordRedactor;
45
import org.xml.sax.SAXException;
56

67
import javax.xml.transform.TransformerException;
@@ -11,6 +12,11 @@
1112

1213
class XmlRedactedSecretFileContent extends FileContent {
1314

15+
private static final String STRING_TAG = "<string>";
16+
private static final String CLOSE_STRING_TAG = "</string>";
17+
18+
private String previousStringTagValue;
19+
1420
public XmlRedactedSecretFileContent(String name, File file) {
1521
super(name, file);
1622
}
@@ -27,4 +33,25 @@ protected InputStream getInputStream() throws IOException {
2733
throw new IOException(e);
2834
}
2935
}
36+
37+
@Override
38+
protected String getSimpleValueOrRedactedPassword(String value) {
39+
if (value.contains(STRING_TAG)) {
40+
return redactStringTagIfNeeded(value);
41+
}
42+
return PasswordRedactor.get().redact(value);
43+
}
44+
45+
private String redactStringTagIfNeeded(String value) {
46+
if (previousStringTagValue != null) {
47+
if (previousStringTagValue.contains(STRING_TAG) && PasswordRedactor.get().match(previousStringTagValue)) {
48+
previousStringTagValue = null;
49+
return value.substring(0, value.indexOf(STRING_TAG)) + STRING_TAG + PasswordRedactor.REDACTED + CLOSE_STRING_TAG;
50+
}
51+
previousStringTagValue = null;
52+
return value;
53+
}
54+
previousStringTagValue = value;
55+
return value;
56+
}
3057
}

src/main/java/com/cloudbees/jenkins/support/filter/FilteredOutputStream.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
@Restricted(NoExternalUse.class)
5555
public class FilteredOutputStream extends FilterOutputStream {
5656

57-
private static final String UNKNOWN_INPUT = "\uFFFD";
57+
public static final String UNKNOWN_INPUT = "\uFFFD";
5858

5959
@GuardedBy("this")
6060
private final ByteBuffer encodedBuf = ByteBuffer.allocate(256);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.cloudbees.jenkins.support.filter;
2+
3+
import hudson.Extension;
4+
import hudson.ExtensionList;
5+
import org.jenkinsci.remoting.SerializableOnlyOverRemoting;
6+
7+
import java.util.Arrays;
8+
import java.util.Collections;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.logging.Level;
13+
import java.util.logging.Logger;
14+
import java.util.regex.Matcher;
15+
import java.util.regex.Pattern;
16+
17+
@Extension
18+
public class PasswordRedactor implements SerializableOnlyOverRemoting {
19+
20+
private static final Logger LOGGER = Logger.getLogger(PasswordRedactor.class.getName());
21+
public static final String REDACTED = "REDACTED";
22+
public static final List<String> FILES_WITH_SECRETS = Collections.unmodifiableList(Arrays.asList("cmdline", "environ"));
23+
24+
private final Pattern pattern;
25+
private final String matcher;
26+
27+
public static PasswordRedactor get() {
28+
return ExtensionList.lookupSingleton(PasswordRedactor.class);
29+
}
30+
31+
public PasswordRedactor() {
32+
this.pattern = PasswordRedactorRegexBuilder.PASSWORD_PATTERN;
33+
this.matcher = PasswordRedactorRegexBuilder.SECRET_PROPERTY_MATCHER;
34+
}
35+
36+
// for tests usage
37+
PasswordRedactor(Pattern pattern, String matcher) {
38+
this.pattern = pattern;
39+
this.matcher = matcher;
40+
}
41+
42+
public String redact(String input) {
43+
if (pattern == null) {
44+
// 'security-stop-words.txt' is empty
45+
return input;
46+
}
47+
Matcher patternMatcher = pattern.matcher(input);
48+
while (patternMatcher.find()) {
49+
LOGGER.log(Level.FINE, "Argument ''{0}'' contain secret data", patternMatcher.group(1));
50+
String secretValue = patternMatcher.group(2);
51+
input = input.replaceFirst("=\\s*" + Pattern.quote(secretValue), "=" + REDACTED);
52+
}
53+
return input;
54+
}
55+
56+
public Map<String, String> redact(Map<String, String> properties) {
57+
if (matcher == null) {
58+
// 'security-stop-words.txt' is empty
59+
return properties;
60+
}
61+
Map<String, String> redacted = new HashMap<>();
62+
for (Map.Entry<String, String> entry : properties.entrySet()) {
63+
if (entry.getKey().matches(matcher)) {
64+
LOGGER.log(Level.FINE, "Argument ''{0}'' contain secret data", entry.getKey());
65+
redacted.put(entry.getKey(), REDACTED);
66+
} else {
67+
redacted.put(entry.getKey(), redact(entry.getValue()));
68+
}
69+
}
70+
return redacted;
71+
}
72+
73+
public boolean match(String value) {
74+
if (matcher == null) {
75+
// 'security-stop-words.txt' is empty
76+
return false;
77+
}
78+
return value.matches(matcher);
79+
}
80+
81+
}

0 commit comments

Comments
 (0)