Skip to content

[JENKINS-75914] Do not mask text less than 2 characters#377

Closed
MarkEWaite wants to merge 1 commit into
jenkinsci:masterfrom
MarkEWaite:master
Closed

[JENKINS-75914] Do not mask text less than 2 characters#377
MarkEWaite wants to merge 1 commit into
jenkinsci:masterfrom
MarkEWaite:master

Conversation

@MarkEWaite

Copy link
Copy Markdown
Contributor

[JENKINS-75914] Do not mask text less than 2 characters

The bug submitter was using a zero length secret text and it significantly increased the log output. Require that the matched regular expression pattern must contain at least two characters.

Testing done

  • Wrote automated tests to confirm original exception is returned when string is less than 3 characters
  • Ran the revised plugin in Jenkins and confirmed that the excess asterisks are not displayed

Submitter checklist

  • Make sure you are opening from a topic/feature/bugfix branch (right side) and not your main branch!
  • Ensure that the pull request title represents the desired changelog entry
  • Please describe what you did
  • Link to relevant issues in GitHub or Jira
  • Link to relevant pull requests, esp. upstream and downstream changes
  • Ensure you have provided tests that demonstrate the feature works or the issue is fixed

The bug submitter was using a zero length secret text and it significantly
increased the log output.  Require that the matched regular expression
pattern must contain at least two characters.

Testing done:

* Wrote automated tests to confirm original exception is returned when
  string is less than 3 characters
* Ran the revised plugin in Jenkins and confirmed that the excess
  asterisks are not displayed
@MarkEWaite
MarkEWaite requested a review from a team as a code owner July 22, 2025 02:26
@pzygielo

Copy link
Copy Markdown

Just out of curiosity - what is the value/purpose of using 4 (constant) number of replacement characters?

var masked = new MaskedException(pattern.matcher(Objects.requireNonNullElse(unmasked.getMessage(), "")).replaceAll("****"));

Wouldn't 1 do just fine?

@MarkEWaite

MarkEWaite commented Jul 22, 2025

Copy link
Copy Markdown
Contributor Author

Just out of curiosity - what is the value/purpose of using 4 (constant) number of replacement characters?

var masked = new MaskedException(pattern.matcher(Objects.requireNonNullElse(unmasked.getMessage(), "")).replaceAll("****"));

Wouldn't 1 do just fine?

I believe that the choice of "****" as the replacement text is for the convenience of the person reading the log. It is easier to detect a sequence of characters than a single character. One of the stated goals of the pattern used to match the masked text is that it should prefer the longest match. By using 4 characters, we don't disclose the length of the matched text and we still provide enough text that a person scanning the log file is likely to see the "****" string as indicating sensitive information that has been masked.

Comment on lines +15 to +17
if (pattern.toString().length() < 3) {
return unmasked;
}

@jtnord jtnord Jul 22, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possibly doesn't work when you have bound multiple credentials (and one is empty?) as the pattern will be built from all of them and still match?
I would rather suggest that the pattern/masker is not even created for an empty credential / match.

Additionally why 2 characters here - is that different to what is masked in the logs? is a user warned in the credential plugin when creating a that a single character password will not be masked?

@MarkEWaite MarkEWaite Jul 22, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if a different heuristic (fallible rule) would be a better choice. If the size of the masked exception message is more than 10% larger than the original exception, then we refuse to mask the original exception. That would replace the heuristic that uses regex pattern length with a heuristic that tries to avoid the specific case that was seen at Eclipse where the size of the logged exception message was much larger than the original exception message.

What do you think of that alternative?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder if a different heuristic (fallible rule) would be a better choice. If the size of the masked exception message is more than 10% larger than the original exception, then we refuse to mask the original exception. That would replace the heuristic that uses regex pattern length with a heuristic that tries to avoid the specific case that was seen at Eclipse where the size of the logged exception message was much larger than the original exception message.

What do you think of that alternative?

I am wondering why the console log did not blow up before this change (ie we previously still masked the console log, the security change just added masking of an exception). Is there a reason we can not apply the same logic, perhaps @Kevin-CB recalls?

If the regular console masks a single character (which I believe it does) then I would say the masked exception should also do the same - the whole point of this code is to behave the same and mask what would have been masked in the console to prevent leakage.
A single character secret is insecure and should not be used (you can extrapolate that to small length too), and would create interesting masked console output.

  • TBH not sure why an empty file is any better but well - that should not cause any masking just like in the console.

I would say that heuristics shouldn't be used, we just need the behaviour to match the console.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public static @NonNull Pattern getAggregateSecretPattern(@NonNull Collection<String> inputs) {
JenkinsJVM.checkJenkinsJVM();
List<SecretPatternFactory> secretPatternFactories = SecretPatternFactory.all();
String pattern = inputs.stream()
.filter(input -> !input.isEmpty())
.flatMap(input ->
secretPatternFactories.stream().flatMap(factory ->
factory.getEncodedForms(input).stream()))
.filter(encoded -> encoded.length() >= MINIMUM_ENCODED_LENGTH)
.sorted(BY_LENGTH_DESCENDING)
.distinct()
.map(Pattern::quote)
.collect(Collectors.joining("|"));
return Pattern.compile(pattern);
}
something strange is going on

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering why the console log did not blow up before this change (ie we previously still masked the console log, the security change just added masking of an exception). Is there a reason we can not apply the same logic, perhaps @Kevin-CB recalls?

There is a check for empty credentials for regular messages on the console log, which wasn't implemented for exceptions.

@jtnord
jtnord requested review from a team and daniel-beck July 22, 2025 11:15
@pzygielo

Copy link
Copy Markdown

I believe that the choice of "****" as the replacement text is for the convenience of the person reading the log.

Right. In regular case

My secret is **** and that's it

indeed is better with ****.

I was under the influence of the result with empty pattern, which gives

****M****y**** ****s****e****c****r****e****t**** ****i****s**** **** ****(****e****m****p****t****y**** ****s****t****r****i****n****g****)**** ****a****n****d**** ****t****h****a****t****'****s**** ****i****t****

It is easier to detect a sequence of characters than a single character.

Got it. Thanks.

@jtnord
jtnord requested review from Kevin-CB and removed request for daniel-beck July 22, 2025 13:39
@Kevin-CB

Kevin-CB commented Jul 22, 2025

Copy link
Copy Markdown
Contributor

the whole point of this code is to behave the same and mask what would have been masked in the console to prevent leakage.

I agree with Jesse James, the behavior should be consistent. I filed #378 to handle empty credentials in the same way.

@jtnord

jtnord commented Jul 22, 2025

Copy link
Copy Markdown
Member

I agree with Jesse, the behavior should be consistent.

🙄

I filed #378 to handle empty credentials in the same way.

closing in favour of #378

@jtnord jtnord closed this Jul 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants