disarm

Bidirectional controls

Detect Trojan Source in source code

Paste code to find the bidirectional control characters that make it render differently from how it compiles. You get a line and column for every one, and the two readings side by side — what a reviewer sees, and what the compiler sees.

The tool

The example is the commenting-out pattern from the 2021 Trojan Source paper: a right-to-left override and two isolates make an if statement appear to sit inside a comment, so the line beneath it looks unreachable and is not.

6 bidirectional controls on 2 lines.

Rendered order does not match logical order. What a reviewer reads above is not what a compiler reads.

What a reviewer sees

Your code as the browser renders it, with the bidi algorithm applied.

#include <stdio.h>

int main() {
    bool isAdmin = false;
    /*‮ } ⁩if (isAdmin)⁦ ⁦ begin admins only */
        printf("You are an admin.\n");
    /* end admins only ‮ { ⁩*/
    return 0;
}

What the compiler sees

The same bytes in logical order, with each control shown where it sits.

4 bool isAdmin = false;
5 /*RLO } PDIif (isAdmin)LRI LRI begin admins only */
6 printf("You are an admin.\n");
7 /* end admins only RLO { PDI*/
8 return 0;

4 lines without controls not shown.

Where the controls are

LineCol CodepointAbbrName
57 U+202ERLO RIGHT-TO-LEFT OVERRIDE
511 U+2069PDI POP DIRECTIONAL ISOLATE
524 U+2066LRI LEFT-TO-RIGHT ISOLATE
526 U+2066LRI LEFT-TO-RIGHT ISOLATE
724 U+202ERLO RIGHT-TO-LEFT OVERRIDE
728 U+2069PDI POP DIRECTIONAL ISOLATE

Running disarm 0.14.1, compiled to WebAssembly. Your code is never uploaded — the engine is loaded into this page and runs on your machine.

A worked example

The tool above needs JavaScript. This is the same finding written out, so it is legible without running anything.

Line 5 of the example, read the two ways. Both are the same bytes; the difference is only whether the bidi controls are honoured.

What a reviewer sees

    /*‮ } ⁩if (isAdmin)⁦ ⁦ begin admins only */

Your browser applies the bidi algorithm here exactly as an editor would. The if appears to sit inside the comment.

What the compiler sees

    /*RLO } PDIif (isAdmin)LRI LRI begin admins only */

Stored order, with each control shown where it sits. The if (isAdmin) is live code, and the comment ends before it.

With the controls removed the line reads /* } if (isAdmin) begin admins only */ — which is what the compiler compiles and what no reviewer was shown.

Controls found in the example source
LineColCodepointName
57U+202ERIGHT-TO-LEFT OVERRIDE
511U+2069POP DIRECTIONAL ISOLATE
524U+2066LEFT-TO-RIGHT ISOLATE
526U+2066LEFT-TO-RIGHT ISOLATE
724U+202ERIGHT-TO-LEFT OVERRIDE
728U+2069POP DIRECTIONAL ISOLATE

Six controls across two lines. The printf beneath them appears to be commented out and is not, so a reviewer approves a program that grants administrator access. No character is misspelled and no identifier is confusable; the file is exactly what it appears to be, read in a different order.

The example carries no direction conflict — that is the other attack. varonis.com.ו contains no control character at all, yet reverses under the bidi algorithm because the final letter is a real Hebrew one. Removing controls cannot fix it, which is why the two are reported apart.

The same thing in your own code

Each code block has been compiled and verified in CI. Provided under the MIT license to illustrate disarm. These operate on the single commenting-out line from the Trojan Source paper, which carries four of the six controls in the sample file the tool above loads — hence controls == 4 beside a table listing six. disarm on GitHub →

# Detect Trojan Source, and the direction conflict that stripping cannot fix.
#   pip install disarm
from disarm import strip_bidi, has_bidi_conflict

# The commenting-out line from the Trojan Source paper: an override and two
# isolates, so the `if` appears to sit inside the comment.
TROJAN = "    /*‮ } ⁩if (isAdmin)⁦ ⁦ begin admins only */"
# No control character at all — the final letter is a real Hebrew vav.
CONFLICT = "varonis.com.ו"

clean = strip_bidi(TROJAN)
controls = len(TROJAN) - len(clean)

# The two checks catch different attacks, which is why both are needed.
assert controls == 4, controls
assert not has_bidi_conflict(TROJAN), "overrides are not a direction conflict"
assert has_bidi_conflict(CONFLICT), "real RTL letters are"
assert strip_bidi(CONFLICT) == CONFLICT, "and there is nothing to strip"

print(f"ok: {controls} bidi controls stripped; a conflict detected where there are none")

Catching it before review, not during

A reviewer cannot be asked to spot this: the whole attack is that the rendered line looks correct. The check belongs in the same place as a formatter or a linter, where it runs on every commit and nobody has to remember it.

The rule is narrow on purpose. A bidi control inside a string literal is sometimes legitimate — right-to-left text in a UI label needs them — so failing on every control produces noise that gets the check disabled. Failing on a control outside a string literal or comment is the signal, because there is no reason for one to sit in code.

WhereWhat to runFail when
Pre-commit hook strip_bidi the stripped file differs from the original, outside string literals.
CI, on the diff strip_bidi an added line contains a control the base did not.
CI, whole tree has_bidi_conflict a file mixes strong directions with no control to strip — the case a stripper cannot fix, so it needs a human.

Both checks are needed, and they catch different attacks. strip_bidi finds the controls; has_bidi_conflict finds the reordering that happens without any. The Python example above runs both against one line and is the body of the hook.

The characters involved

Bidirectional controls exist for a real purpose: text mixing Arabic or Hebrew with Latin needs them. In source code they have no legitimate use outside string literals and comments meant to contain such text, which is why their presence is worth flagging rather than silently removing.

CodepointAbbrNameEffect
U+202DLROLeft-to-right overrideForces the following text left to right, whatever it contains.
U+202ERLORight-to-left overrideForces the following text right to left. The usual vehicle for the attack.
U+202ALRELeft-to-right embeddingOpens a left-to-right run; ended by PDF.
U+202BRLERight-to-left embeddingOpens a right-to-left run; ended by PDF.
U+202CPDFPop directional formattingEnds the most recent embedding or override.
U+2066LRILeft-to-right isolateOpens an isolated run, ended by PDI.
U+2067RLIRight-to-left isolateOpens an isolated right-to-left run.
U+2068FSIFirst strong isolateDirection taken from the first strong character inside.
U+2069PDIPop directional isolateEnds the most recent isolate.
U+200ELRMLeft-to-right markAn invisible strong character used to nudge ordering.
U+200FRLMRight-to-left markThe right-to-left counterpart.
U+061CALMArabic letter markAs RLM, for Arabic-script context.
U+00ADSHYSoft hyphenInvisible unless a line breaks there.

The Trojan Source paper recommends that build pipelines refuse, or at least warn on, unterminated overrides. Compilers have since added their own checks — rustc and GCC both warn — but they cover their own source, not the data your program reads.

Found a string this gets wrong? The confusables table grew out of exactly that kind of report. Open an issue with it.

Related tools