Skip to content

Commit c08919f

Browse files
Add trim helper to remove trailing newlines from secrets (#2810)
1 parent 70a678d commit c08919f

3 files changed

Lines changed: 106 additions & 6 deletions

File tree

docs/features/secrets.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ If you need to read a secret from a file, or perform additional en/decoding of t
5757
- `decodeBase64`
5858
- `readFileBase64`
5959
- `json`
60+
- `trim`
6061

6162
Helpers can be combined to do multiple transformations. Examples:
6263

@@ -139,6 +140,16 @@ Example:
139140
${json:username:${ENV_VAR}}
140141
```
141142

143+
==== trim
144+
145+
Removes trailing whitespace characters (including newlines, spaces, and tabs) from the provided secret. Leading whitespace is preserved.
146+
147+
Example:
148+
149+
```
150+
${trim:${readFile:/secret/passphrase.txt}}
151+
```
152+
142153
=== Security and compatibility considerations
143154

144155
// TODO(oleg_nenashev): Add a link to the advisory once ready

plugin/src/main/java/io/jenkins/plugins/casc/SecretSourceResolver.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.util.stream.Stream;
1919
import org.apache.commons.lang3.StringUtils;
2020
import org.apache.commons.text.StringSubstitutor;
21-
import org.apache.commons.text.TextStringBuilder;
2221
import org.apache.commons.text.lookup.StringLookup;
2322
import org.json.JSONObject;
2423
import org.kohsuke.accmod.Restricted;
@@ -40,7 +39,7 @@ public class SecretSourceResolver {
4039

4140
public SecretSourceResolver(ConfigurationContext configurationContext) {
4241
// TODO update to use Map.of in JDK11+
43-
Map<String, org.apache.commons.text.lookup.StringLookup> map = new HashMap<>(8);
42+
Map<String, org.apache.commons.text.lookup.StringLookup> map = new HashMap<>(16);
4443
map.put("base64", Base64Lookup.INSTANCE);
4544
map.put("fileBase64", FileBase64Lookup.INSTANCE);
4645
map.put("readFileBase64", FileBase64Lookup.INSTANCE);
@@ -49,6 +48,7 @@ public SecretSourceResolver(ConfigurationContext configurationContext) {
4948
map.put("sysProp", SystemPropertyLookup.INSTANCE);
5049
map.put("decodeBase64", DecodeBase64Lookup.INSTANCE);
5150
map.put("json", JsonLookup.INSTANCE);
51+
map.put("trim", TrimLookup.INSTANCE);
5252
map = Collections.unmodifiableMap(map);
5353

5454
substitutor = new StringSubstitutor(new FixedInterpolatorStringLookup(
@@ -106,10 +106,9 @@ public String resolve(String toInterpolate) {
106106
if (StringUtils.isBlank(toInterpolate) || !toInterpolate.contains(enclosedBy)) {
107107
return toInterpolate;
108108
}
109-
final TextStringBuilder buf = new TextStringBuilder(toInterpolate);
110-
substitutor.replaceIn(buf);
111-
nullSubstitutor.replaceIn(buf);
112-
return buf.toString();
109+
String result = substitutor.replace(toInterpolate);
110+
result = nullSubstitutor.replace(result);
111+
return result;
113112
}
114113

115114
static class UnresolvedLookup implements StringLookup {
@@ -250,4 +249,16 @@ public String lookup(@NonNull final String key) {
250249
return output;
251250
}
252251
}
252+
253+
static class TrimLookup implements StringLookup {
254+
255+
static final TrimLookup INSTANCE = new TrimLookup();
256+
257+
private TrimLookup() {}
258+
259+
@Override
260+
public String lookup(@NonNull final String key) {
261+
return StringUtils.stripEnd(key, null);
262+
}
263+
}
253264
}

plugin/src/test/java/io/jenkins/plugins/casc/SecretSourceResolverTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,4 +443,82 @@ private static void assertVarEncoding(String expected, String toEncode) {
443443
String encoded = context.getSecretSourceResolver().encode(toEncode);
444444
assertThat(encoded, equalTo(expected));
445445
}
446+
447+
@Test
448+
public void trimLookup_removesTrailingWhitespace() {
449+
assertThat(SecretSourceResolver.TrimLookup.INSTANCE.lookup("value\n"), equalTo("value"));
450+
assertThat(SecretSourceResolver.TrimLookup.INSTANCE.lookup("value\r\n"), equalTo("value"));
451+
assertThat(SecretSourceResolver.TrimLookup.INSTANCE.lookup(" value \n"), equalTo(" value"));
452+
}
453+
454+
@Test
455+
public void resolve_trimRemovesTrailingNewlines() {
456+
environment.set("FOO", "my_secret_pass\n");
457+
assertThat(resolve("${trim:${FOO}}"), equalTo("my_secret_pass"));
458+
459+
environment.set("BAR", "my_secret_pass\r\n");
460+
assertThat(resolve("${trim:${BAR}}"), equalTo("my_secret_pass"));
461+
462+
environment.set("BAZ", "my_secret_pass\n\n\n");
463+
assertThat(resolve("${trim:${BAZ}}"), equalTo("my_secret_pass"));
464+
}
465+
466+
@Test
467+
public void resolve_trimNoNewline_noChange() {
468+
environment.set("FOO", "my_secret_pass");
469+
assertThat(resolve("${trim:${FOO}}"), equalTo("my_secret_pass"));
470+
}
471+
472+
@Test
473+
public void resolve_trimRemovesTrailingSpacesButPreservesLeading() {
474+
environment.set("FOO", " my_secret_pass \n");
475+
assertThat(resolve("${trim:${FOO}}"), equalTo(" my_secret_pass"));
476+
477+
environment.set("BAR", "\tmy_secret_pass\t\r\n");
478+
assertThat(resolve("${trim:${BAR}}"), equalTo("\tmy_secret_pass"));
479+
}
480+
481+
@Test
482+
public void resolve_trimNestedReadFile() throws Exception {
483+
Path tempSecretFile = Files.createTempFile("casc-secret", ".txt");
484+
tempSecretFile.toFile().deleteOnExit();
485+
486+
Files.writeString(tempSecretFile, "super_secret_ssh_key\n");
487+
String inputPath = tempSecretFile.toAbsolutePath().toString();
488+
489+
String rawOutput = resolve("${readFile:" + inputPath + "}");
490+
assertTrue("Raw output should contain the problematic trailing newline", rawOutput.endsWith("\n"));
491+
assertThat(rawOutput, equalTo("super_secret_ssh_key\n"));
492+
493+
String trimmedOutput = resolve("${trim:${readFile:" + inputPath + "}}");
494+
assertThat(trimmedOutput, equalTo("super_secret_ssh_key"));
495+
}
496+
497+
@Test
498+
public void resolve_readFilePreservesTrailingNewline() throws Exception {
499+
Path tempFile = Files.createTempFile("casc-secret-baseline", ".txt");
500+
tempFile.toFile().deleteOnExit();
501+
502+
Files.writeString(tempFile, "secret\n");
503+
504+
String output = resolve("${readFile:" + tempFile.toAbsolutePath() + "}");
505+
assertThat(output, equalTo("secret\n"));
506+
}
507+
508+
@Test
509+
public void resolve_trimEmptyString() {
510+
assertThat(resolve("${trim:}"), equalTo(""));
511+
}
512+
513+
@Test
514+
public void resolve_trimOnlyNewlines() {
515+
environment.set("FOO", "\n\n");
516+
assertThat(resolve("${trim:${FOO}}"), equalTo(""));
517+
}
518+
519+
@Test
520+
public void resolve_trimWithSpacesOnly_becomesEmpty() {
521+
environment.set("FOO", " ");
522+
assertThat(resolve("${trim:${FOO}}"), equalTo(""));
523+
}
446524
}

0 commit comments

Comments
 (0)