Skip to content

Fix predictable marker collision in EncodingPatternPreservation#914

Open
Socialpranker wants to merge 1 commit into
ESAPI:developfrom
Socialpranker:fix-csscodec-marker-collision
Open

Fix predictable marker collision in EncodingPatternPreservation#914
Socialpranker wants to merge 1 commit into
ESAPI:developfrom
Socialpranker:fix-csscodec-marker-collision

Conversation

@Socialpranker

Copy link
Copy Markdown

Summary

EncodingPatternPreservation (used by CSSCodec to protect rgb(...) triplets from CSS-escaping during encodeForCSS) previously used a fixed, publicly-known default marker: this class's own simple name, "EncodingPatternPreservation".

captureAndReplaceMatches() temporarily swaps matched content for this marker; restoreOriginalContent() restores it afterwards using replaceFirst(marker, ...), matching the marker as plain literal text, in FIFO order.

Because the marker is a known constant, input that already contains that literal string ahead of real matched content causes replaceFirst to match the attacker-supplied text instead of the real placeholder. Every subsequent restoration in the same call is then shifted by one, silently corrupting the output — restored content ends up in the wrong position (or is left as a raw, unrestored marker string).

Minimal repro:

Pattern rgb = Pattern.compile("([rR][gG][bB])\\s*\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*\\)");
EncodingPatternPreservation epp = new EncodingPatternPreservation(rgb);

String input = "EncodingPatternPreservation color:rgb(1,2,3);";
String replaced = epp.captureAndReplaceMatches(input);
// replaced == "EncodingPatternPreservation color:EncodingPatternPreservation;"

String restored = epp.restoreOriginalContent(replaced);
// restored == "rgb(1,2,3) color:EncodingPatternPreservation;"   (WRONG — desynced)
// expected == "EncodingPatternPreservation color:rgb(1,2,3);"

I want to be precise about severity: I checked whether this is exploitable as an injection/XSS primitive and don't believe it is — the only regex currently fed through this class (CSSCodec's RGB-triplet pattern) can only ever capture digits, %, commas, whitespace and rgb(/), so a desynchronized restore can't smuggle unexpected characters through encodeForCSS(). This is an output-integrity bug (silently wrong/corrupted encoder output), not a bypass of the encoding itself.

Fix

Derive the default marker per-instance from a random UUID instead of a fixed literal, so it can't be predicted or pre-supplied in input. Hyphens are stripped from the UUID because a hyphen would itself be altered by an encoding pass (e.g. CSSCodec.encode() backslash-escapes non-immune characters) before restoreOriginalContent() runs, which would break the exact-match lookup — I hit this while writing the fix and confirmed it against the existing CSSCodecTest suite. The public setReplacementMarker() API is unchanged for callers who want to supply their own marker.

Test plan

  • Added testDefaultMarkerIsUnpredictablePerInstance — two instances never produce the same default marker.
  • Added testInputContainingLiteralClassNameDoesNotDesyncRestoration — regression test reproducing the exact scenario above.
  • Updated the two existing tests that asserted on the old fixed-marker string, to assert on the marker's presence/shape instead.
  • mvn -Dtest=EncodingPatternPreservationTest,CSSCodecTest test — 11/11 pass.
  • Full mvn test — pass, no other suite affected.

EncodingPatternPreservation.captureAndReplaceMatches() previously used a
fixed, publicly-known default marker (this class's simple name,
"EncodingPatternPreservation") to temporarily stand in for matched
content while the rest of the string is encoded.

restoreOriginalContent() restores captured content in FIFO order using
replaceFirst(marker, ...), matching the marker as plain literal text.
If the input already contains that literal string ahead of the real
matched content (e.g. CSSCodec.encode() on
"EncodingPatternPreservation background:rgb(1,2,3)"), replaceFirst
matches the attacker-supplied text instead of the real placeholder,
silently desynchronizing every subsequent restoration and corrupting
the encoded output.

Fix: derive the default marker per-instance from a random UUID (with
hyphens stripped, since a hyphen would itself be altered by an
encoding pass such as CSSCodec's before restoration), so it cannot be
predicted or embedded by input content. The public
setReplacementMarker() API is unchanged.

Added regression tests covering marker unpredictability and the
literal-marker-in-input desync scenario.
@Socialpranker

Copy link
Copy Markdown
Author

Filed the underlying bug as #915 with repro details, per the issue → PR workflow described in CONTRIBUTING-TO-ESAPI.txt.

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.

1 participant