Skip to content

fix: detect bidi control characters outside markdown files (CVE-2021-42574) - #473

Open
MohammedAlkindi wants to merge 2 commits into
NVIDIA:mainfrom
MohammedAlkindi:fix/bidi-controls-all-file-types
Open

fix: detect bidi control characters outside markdown files (CVE-2021-42574)#473
MohammedAlkindi wants to merge 2 commits into
NVIDIA:mainfrom
MohammedAlkindi:fix/bidi-controls-all-file-types

Conversation

@MohammedAlkindi

Copy link
Copy Markdown

P2's bidi-control pattern (Trojan Source, CVE-2021-42574) sits in P2_PATTERNS, which only runs under if file_type in ("markdown", "other"). static_runner maps .py to python, .sh to shell and so on, so a bidi payload in a bundled script was never checked — the file types the CVE actually targets.

Same payload (U+202EU+202C reversing a comment) in one skill:

payload in before after
SKILL.md 49, CAUTION, P2 49, CAUTION, P2
scripts/helper.py 10, SAFE, no P2 38, CAUTION, P2

Markdown is unchanged and still emits exactly one P2.

#39 reported this exact helper.py case. #92 added the regex to the markdown-gated list without widening the gate, and #39 auto-closed with no comments.

Only the bidi entry moves. Zero-width stays gated deliberately: ZERO_WIDTH_CHARS includes U+FEFF, so ungating it would flag every BOM-prefixed source file — the false positive the Tag-block comment already calls out.

Your call on one tradeoff: bidi controls have legitimate RTL/i18n uses, so this can fire on source with mixed-direction text. I added no locale carve-out, matching the existing unconditional Tag-block check.

The bidi-control pattern (Trojan Source, CVE-2021-42574) lived in
P2_PATTERNS, which only runs when file_type is markdown or other.
static_runner maps .py to python, .sh to shell and so on, so a bidi
payload in a bundled script was never checked - the file types the CVE
actually targets.

Evaluate the bidi pattern regardless of file_type, alongside the existing
unconditional Unicode Tag-block check. Zero-width detection stays
markdown-gated deliberately: ZERO_WIDTH_CHARS includes U+FEFF, so
ungating it would flag every BOM-prefixed source file.

Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com>

@yashrajp22 yashrajp22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fix looks right to me. I ran it end to end: a bidi payload in a bundled .py goes from 0 findings to 1 HIGH P2, all 9 paired control chars are caught individually, and markdown output is byte-identical to main (same lines, same confidence, no double-reporting). The zero-width/BOM gating is untouched — checked with BOM-prefixed .py and .md files. It also quietly starts covering .txt files, which the description doesn't mention — nice bonus. A few small notes inline, none blocking.

# Bidirectional control characters (Trojan Source, CVE-2021-42574). Evaluated
# unconditionally below, NOT in the markdown-gated P2_PATTERNS list -- see the
# comment at that check for why.
_BIDI_CONTROL_PATTERN = r"[\u202a-\u202e\u2066-\u2069]"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One thing worth a one-line code comment here: this class covers the 9 paired controls (U+202A–202E and U+2066–2069) but not the direction marks U+200E/U+200F/U+061C — I checked, those produce zero findings anywhere (they're not in ZERO_WIDTH_CHARS either). That matches GCC's -Wbidi-chars default, since the marks can't reorder spans the way the paired controls can, so I think the exclusion is right. But since the PR references CVE-2021-42574, documenting that it's deliberate would keep someone from "fixing" it later without understanding the tradeoff.

# (issue #39). Zero-width detection stays markdown-gated because
# ZERO_WIDTH_CHARS includes U+FEFF (BOM), which would false-positive on
# every BOM-prefixed source file; the bidi range never overlaps it.
for match in _p2_pattern_matches(content, _BIDI_CONTROL_PATTERN):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

On the i18n tradeoff you called out in the description — I confirmed it's real but narrow: a legit FSI/PDI-wrapped Arabic string in a .json l10n file now fires HIGH/0.85, while plain Arabic/Hebrew text without control chars stays clean. If false positives ever become a problem, the surgical option is a lower confidence (~0.6) for just the isolate range U+2066–2069 (the well-formed idiom message-formatting libraries emit), keeping 0.85 for U+202A–202E, which is what Trojan Source actually needs. Fine as-is for a security scanner — just leaving the option here.

findings.append(
AnalyzerFinding(
rule_id="P2",
message="Hidden Instructions",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tiny nit, feel free to ignore: "Hidden Instructions" is indistinguishable from the zero-width and HTML-comment P2 findings when triaging results — the Tag-block check above uses a self-describing message. Changing it would shift the message on existing markdown findings though (your parity test asserts that), so leaving it is also reasonable.

@MohammedAlkindi

Copy link
Copy Markdown
Author

Added the comment (850a32b), with the -Wbidi-chars parity as the stated reason so nobody "fixes" it later.

I re-measured your check before writing it: U+200E, U+200F and U+061C each produce 0 findings in both a .md and a bundled .py, and none are in ZERO_WIDTH_CHARS. The 9 paired controls fire.

Leaving the isolate-range confidence split and the "Hidden Instructions" wording alone for now, since both move existing markdown findings that the parity test pins. Worth revisiting if l10n false positives actually turn up.

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.

2 participants