Skip to content

fix: decode ES2015+ \u{...} extended unicode escapes in jsDecode - #1657

Open
fzipi wants to merge 2 commits into
mainfrom
fix/jsdecode-extended-unicode-escape
Open

fix: decode ES2015+ \u{...} extended unicode escapes in jsDecode#1657
fzipi wants to merge 2 commits into
mainfrom
fix/jsdecode-extended-unicode-escape

Conversation

@fzipi

@fzipi fzipi commented Jul 25, 2026

Copy link
Copy Markdown
Member

Summary

  • doJsDecode only recognized \uHHHH (exactly 4 hex digits). The \u{H...H} extended code point escape (1-6 hex digits in braces), supported by every modern JS engine since ES2015, fell through to the generic escape branch — the backslash was dropped and the literal u kept, leaving {H...H} uncorrected in the output.
  • Added a case recognizing \u{H...H}, handled the same way as the existing \uHHHH case: lower byte of the resolved value, with the same full-width-ASCII fold.
  • Malformed escapes (no digits, more than 6 digits, missing closing }) fall through to the existing generic-escape handling unchanged.

Fixes #1653.

Test plan

  • Added regression tests: \u{61}\u{6c}\u{65}\u{72}\u{74}alert (the reported bypass), single/double hex digit cases, full-width-ASCII fold (\u{ff01}!), and malformed-escape fallback cases
  • go test ./..., go vet ./..., golangci-lint run all pass

Benchmark (BenchmarkJSDecode, Apple M2, benchtime=1s)

case main this branch
empty string 2.14-2.28 ns/op 2.31-2.73 ns/op
hello world 3.29-4.30 ns/op 2.80-2.99 ns/op
existing mixed-escape case 162-166 ns/op, 152 B/op, 7 allocs 168-247 ns/op, 152 B/op, 7 allocs
\u{61}\u{6c}\u{65}\u{72}\u{74} (new) n/a (not decoded before) 65-73 ns/op, 32 B/op, 1 alloc

No allocation-count change on any pre-existing case; the new switch-case adds a few cheap byte comparisons per backslash encountered, within run-to-run noise on this machine.

Summary by CodeRabbit

  • New Features
    • Added support for decoding JavaScript ES2015+ extended Unicode escapes in the format \u{...} (1–6 hex digits).
    • Applies existing full-width-ASCII normalization when the decoded value matches.
  • Bug Fixes
    • Improved handling of malformed or incomplete extended escapes so they’re no longer incorrectly decoded, preserving the original text.
  • Tests
    • Expanded test coverage for extended escape decoding and updated benchmarks accordingly.

@fzipi
fzipi requested a review from a team as a code owner July 25, 2026 14:23
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c9e4188c-a9b4-4b53-9fbb-68e960563e2a

📥 Commits

Reviewing files that changed from the base of the PR and between 6928309 and 0e77092.

📒 Files selected for processing (2)
  • internal/transformations/js_decode.go
  • internal/transformations/js_decode_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/transformations/js_decode.go
  • internal/transformations/js_decode_test.go

📝 Walkthrough

Walkthrough

Changes

Extended Unicode escape decoding

Layer / File(s) Summary
Decoder validation and transformation
internal/transformations/js_decode.go
Adds validation and decoding for \u{H...H} escapes with 1–6 hexadecimal digits, including full-width ASCII folding while retaining existing escape handling.
Decoder test and benchmark coverage
internal/transformations/js_decode_test.go
Adds valid and malformed escape cases and benchmarks repeated extended escapes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • #1668 — Concerns escape parsing behavior in the same JavaScript decoder, including malformed and extended Unicode sequences.

Suggested reviewers: jcchavezs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: decoding ES2015+ extended Unicode escapes in jsDecode.
Linked Issues check ✅ Passed The PR implements #1653 by decoding braced Unicode escapes and adding tests for valid and malformed cases.
Out of Scope Changes check ✅ Passed The changes are limited to the decoder, its tests, and benchmark inputs, with no obvious unrelated additions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@fzipi fzipi added the v3.8 label Jul 25, 2026
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.80%. Comparing base (2cea9a0) to head (0e77092).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1657      +/-   ##
==========================================
+ Coverage   87.77%   87.80%   +0.03%     
==========================================
  Files         178      178              
  Lines        9144     9170      +26     
==========================================
+ Hits         8026     8052      +26     
  Misses        854      854              
  Partials      264      264              
Flag Coverage Δ
coraza.no_memoize 87.89% <100.00%> (+0.03%) ⬆️
coraza.rule.case_sensitive_args_keys 87.77% <100.00%> (+0.03%) ⬆️
coraza.rule.mandatory_rule_id_check 87.79% <100.00%> (+0.03%) ⬆️
coraza.rule.multiphase_evaluation 87.56% <100.00%> (+0.03%) ⬆️
coraza.rule.no_regex_multiline 87.76% <100.00%> (+0.03%) ⬆️
coraza.rule.rx_prefilter 87.80% <100.00%> (+0.03%) ⬆️
default 87.80% <100.00%> (+0.03%) ⬆️
examples+ 16.37% <0.00%> (-0.06%) ⬇️
examples+coraza.no_memoize 85.77% <100.00%> (+0.04%) ⬆️
examples+coraza.rule.case_sensitive_args_keys 85.74% <100.00%> (+0.04%) ⬆️
examples+coraza.rule.mandatory_rule_id_check 85.85% <100.00%> (+0.04%) ⬆️
examples+coraza.rule.multiphase_evaluation 87.56% <100.00%> (+0.03%) ⬆️
examples+coraza.rule.no_regex_multiline 85.68% <100.00%> (+0.04%) ⬆️
examples+coraza.rule.rx_prefilter 86.03% <100.00%> (+0.03%) ⬆️
examples+no_fs_access 85.09% <100.00%> (+0.04%) ⬆️
ftw 87.80% <100.00%> (+0.03%) ⬆️
no_fs_access 87.15% <100.00%> (+0.03%) ⬆️
tinygo 87.80% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/transformations/js_decode_test.go`:
- Around line 29-62: Add test cases to the JavaScript decode table near the
existing extended Unicode escapes for leading-zero inputs \u{0ff01} and
\u{00ff01} expecting "!", a valid six-digit escape, and a malformed seven-digit
escape. Ensure the cases verify the helper’s 1–6 hexadecimal digit boundary and
reject the seven-digit form with the expected malformed-escape output.

In `@internal/transformations/js_decode.go`:
- Around line 53-56: The full-width ASCII folding in the decode logic only
handles exactly four hex digits, so update the folding in
jsExtendedUnicodeEscapeLen to numerically recognize U+FF01–U+FF5E across all
accepted 1–6 digit escape lengths, including leading-zero forms, and fold them
correctly. Add or extend boundary tests in js_decode_test.go covering the
1-digit through 6-digit valid lengths, especially leading-zero encodings and the
six-digit boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 042e6ac3-9441-4898-a33f-d9248c865752

📥 Commits

Reviewing files that changed from the base of the PR and between 85a8968 and ea63d82.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (2)
  • internal/transformations/js_decode.go
  • internal/transformations/js_decode_test.go

Comment thread internal/transformations/js_decode_test.go
Comment thread internal/transformations/js_decode.go
@fzipi
fzipi force-pushed the fix/jsdecode-extended-unicode-escape branch from 9f11239 to 6928309 Compare July 26, 2026 13:25
@fzipi
fzipi enabled auto-merge (squash) July 26, 2026 13:47
fzipi and others added 2 commits July 26, 2026 10:51
doJsDecode only recognized \uHHHH (exactly 4 hex digits). The \u{H...H}
extended code point escape (1-6 hex digits in braces), supported by
every modern JS engine since ES2015, fell through to the generic
escape branch: the backslash was dropped and the literal "u" kept,
leaving the rest ("{H...H}") uncorrected in the output -- so a keyword
spelled with \u{...} escapes (e.g. \u{61}\u{6c}\u{65}\u{72}\u{74} for
"alert") never got decoded at all.

See #1653

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The full-width-ASCII fold only applied when the escape had exactly 4
hex digits, so a leading-zero encoding of the same value -- \u{0ff01}
or \u{00ff01}, both numerically U+FF01 -- skipped the fold entirely
and decoded to the raw low byte instead of '!'. Trivially defeats the
fold's purpose (normalizing fullwidth-character evasion).

Now computes the fully resolved code point (same approach as
cssDecode's fix in #1658) and checks the fold range against that
value directly, independent of digit count or leading zeros.

Found by CodeRabbit review on this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@fzipi
fzipi force-pushed the fix/jsdecode-extended-unicode-escape branch from 6928309 to 0e77092 Compare July 26, 2026 13:51

switch {

case (i+2 < inputLen) && (input[i+1] == 'u') && (input[i+2] == '{') && jsExtendedUnicodeEscapeLen(input, i+3) > 0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

just for my understanding, you are calling the jsExtendedUnicodeEscapeLen on the match case and below, so you will end up calling it twice anyway. Would it make sense to instead just branch the below inside an if and remove it from the case arm, like:

if j := jsExtendedUnicodeEscapeLen(input, i+3); j > 0 { ... do its thing ... }

{
// 7 hex digits exceeds the 6-digit maximum: malformed, falls
// through to the generic escape handling unchanged.
input: "\\u{1234567}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

maybe some more tests:
\u{} // Some null cases \u{0} // Some null cases \u{41}\u0042 // Or something that tests the combination of both on a single line \u{0FF5e} // should produce ~ as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

jsDecode doesn't decode ES2015+ \u{...} extended Unicode escapes

2 participants