Repository navigation
Decode a base64 run only where the scanner decodes it - #36
Merged
Merged
Conversation
A run whose padding cannot complete its last group is left as written, surplus padding is ignored, the result is judged by code points, and the printable check follows the scanner. A reversed text with a letters-and-slash run no longer loses its phrase to a decode the scanner never makes.
This was referenced Oct 14, 2026
azrollin
added a commit
that referenced
this pull request
Oct 14, 2026
…38) When a base64 segment holds invalid bytes, only those bytes are dropped and a replacement character that was encoded in the text (the bytes EF BF BD) stays where it was written, as the scanner's decode with errors="ignore" does. The strict decode is still tried first; when it fails, the bytes are walked against the well-formed UTF-8 table and whatever is not well-formed is skipped, so the work stays linear in the segment length. This closes the 256 rows the review of #36 left open: texts the scanner blocks that this runtime allowed because the encoded replacement character was deleted along with the invalid bytes. The new decode agrees with the scanner on 60,001 byte strings, and on the 4,472 independent rows of that review this head changes exactly those 256 rows and lowers none. One difference remains in this entry and is still listed: the printable test reads this runtime's Unicode tables where the scanner reads Python's, so a character assigned in one and not the other can decide a segment differently. The rule set is untouched.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the inherited pair of GLS-PI-007 rows named in #35 (one padded, one not) where the scanner blocks and the Worker allows. The same pair differs on main before #35. The rule set is untouched; this aligns which base64-looking runs the Worker's preprocessor accepts for decoding (padding acceptance) with the scanner. Two decoding differences remain and are listed under Limitations: invalid-byte skipping and the printable-character table.
Cause
The scanner reads a run of 20 or more base64 characters with Python's
b64decode, which refuses a run whose padding cannot complete the last group and then leaves the run as written. The Worker usedatob(), which is more forgiving: it accepts a run with no padding and refuses surplus padding. In the failing rows the text is written backwards and contains a 23 character run of letters and a slash. The scanner leaves that run alone and still reads the reversed phrase. The Worker decoded it to noise, which destroyed the phrase.The same difference ran the other way: a run with surplus padding that the scanner decodes was left as written here.
Change
src/preprocessor.js,decodeBase64Segmentsonly:b64decodeaccepts it: one leftover character in the last group never decodes, two need both=, three need at least one, and padding beyond that is ignored.str.isprintable()through Unicode categories (other, separator) instead of a short list of control characters.The matching entry in
LIMITATIONSand the README line are updated to what remains.Test, red on main and green on this head
tests/base64_segment_padding.test.mjswithtests/fixtures/scan_corpus_base64_padding.json. Expected values come from the scanner's own function and scan (0.6.8, commit 0a3b4686). It covers both padded and unpadded forms of the failing rows (including 2,100 characters of padding that the normalizer removes, in two kinds), runs with each amount of padding, and the result checks above.Differential against the scanner
decode_base64_segmentson 546 generated texts (14 payload shapes x padding states, 300 random byte strings, raw alphabet runs of 20 to 27 characters), output text and recorded segments compared: 105 differences on main, 0 on this head.Decision and sorted finding ids, one scan per row, stock rules:
scan_corpus_068.json)What remains different
This PR makes no claim that the hosted demo matches the scanner beyond the rows and checks listed here.
Gates on this head
Private checkout of the scanner at 0a3b4686. All exit 0 on head 48409df: disclosure gate; policy parity (14 cases, 0 mismatches); engine parity (137 cases, 0 verdict, 0 finding-set deltas); channel parity (2,488 cases, 0, 0); wide parity (1,555 pairs, 0, 0, no known entries); counterexample parity (0 deltas, 0 slower than the bound);
parity_test.py(882 regexes, 0 misses; 13,024 benign checks, 0 disagreements; seed 4173371, replay withPARITY_SEED); v-flag disclosure; metadata contract.node --test tests/*.test.mjs: 164 passed, 0 failed (156 on main, plus the 8 new).python3 -m pyteston the dotless-i tests: 62 passed. CI:wide-parityandworkerd-runtimegreen.