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.

Scan a diff
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:

Response (abridged)
{
  "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.

.github/workflows/unicode-gate.yml
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.sarif

Tune 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.

More guides for this API