Agents / Guide
Block Trojan Source and hidden-Unicode attacks in a pull request
Fail a CI job when a diff hides code behind bidirectional controls, invisible characters or look-alike identifiers, and get the exact file, line and column.
Why code review misses this
A bidirectional control character such as U+202E makes an editor draw source code in a different order than the compiler reads it. A condition that looks like a harmless string comparison can be a different program, and no human reviewer can see the character. This class of attack is known as Trojan Source (CVE-2021-42574). Invisible characters, variation-selector payloads and homoglyph identifiers hide code the same way.
The Unicode Safety Gate applies 19 rules to a diff, a file list, a public GitHub repo or a pull request URL, and returns a block, warn or pass verdict with locations. It is deterministic: the same input gives the same findings.
Scan the diff of a pull request
Send the unified diff as the diff field. The call below is for the RapidAPI listing; set RAPIDAPI_HOST to the host shown on the listing page.
gh pr diff 128 > pr.diff
jq -Rs '{diff: .}' pr.diff > body.json
curl -sS -X POST "https://$RAPIDAPI_HOST/v1/unicode-safety-gate" \
-H "Content-Type: application/json" \
-H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
-H "X-RapidAPI-Host: $RAPIDAPI_HOST" \
-d @body.json | jq '.data | {verdict, riskScore, nextStep}'What a blocked change looks like
We ran the gate on a one-line change that adds if (user.role === "user\u202e \u2066// check admin\u2069 \u2066") return true; to an isAdmin function. In an editor this reads as a comment about an admin check. The response:
{
"data": {
"verdict": "block",
"verdictReason": "6 finding(s) fail the policy",
"riskScore": 90,
"summary": { "total": 6, "blocking": 6, "bySeverity": { "critical": 6 }, "filesScanned": 1 },
"findings": [
{
"ruleId": "USC001",
"ruleName": "bidi-control",
"severity": "critical",
"path": "src/auth.js",
"line": 2,
"column": 26,
"codePoints": "U+202E",
"message": "Bidirectional control character U+202E RIGHT-TO-LEFT OVERRIDE in identifier"
}
],
"nextStep": "Block the merge. Fix src/auth.js:2 (USC001, USC015): Delete the bidirectional control characters; they have no legitimate use in source code."
}
}Fail the build in GitHub Actions
The job below blocks the merge when the verdict is block, prints the findings in the log, and uploads the SARIF 2.1.0 log that every response carries so the findings appear in the Security tab. Store the key as a repository secret.
name: unicode-gate
on: pull_request
permissions:
contents: read
pull-requests: read
security-events: write
jobs:
scan:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
RAPIDAPI_KEY: ${{ secrets.RAPIDAPI_KEY }}
RAPIDAPI_HOST: code-review-and-software-supply-chain-security-api.p.rapidapi.com
steps:
- name: Scan the pull request diff
run: |
gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} > pr.diff
jq -Rs '{diff: .}' pr.diff > body.json
curl -sS --fail-with-body -X POST "https://$RAPIDAPI_HOST/v1/unicode-safety-gate" \
-H "Content-Type: application/json" \
-H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
-H "X-RapidAPI-Host: $RAPIDAPI_HOST" \
-d @body.json -o result.json
jq '.data.sarif' result.json > unicode.sarif
jq -r '.data.findings[] | "\(.path):\(.line):\(.column) \(.ruleId) \(.message)"' result.json
test "$(jq -r '.data.verdict' result.json)" != "block"
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: unicode.sarifTune it without weakening it
Projects that legitimately use non-Latin identifiers can set identifierPolicy to permitted-scripts and list the allowed scripts in permittedScripts; the default ascii-only flags every non-ASCII identifier. A warn verdict means the change has suspicious characters that are not blocking under your policy, so surface it as a review comment instead of failing the job.
Only added lines in a diff are scanned, and findings carry new-file line numbers. Characters that are already in the repository are not in the diff; scan those once by sending repo (a public GitHub repo) instead of diff.
Try it
- Subscribe on RapidAPI and call it with your key.
- Prefer to pay per call in USDC with no account? The same request body goes to
POST https://agents.writerslogic.com/api/v1/unicode-safety-gate; see the endpoint documentation. - Every response is signed and can be checked for free at
POST https://agents.writerslogic.com/api/v1/verify-provenance.