Skip to content

About

opengrep rules that help detect supply-chain and toolchain attacks against React Native developers on macOS — the companion rule set for the RN Developer's Security Guide.

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rn-dev-security-guide-opengrep

119 opengrep rules that detect supply-chain and toolchain attacks against React Native developers on macOS — malicious install scripts, repo-controlled editor/agent config, invisible-Unicode payloads, unpinned CI actions, signing-material leaks, and more, across the whole polyglot RN toolchain (npm/Yarn/pnpm, Ruby/CocoaPods, Gradle, Xcode, git, CI).

Point it at a React Native repository — your own, or one someone just handed you — and it turns an unbounded "read everything" review into a bounded "read these forty lines" one.


What this is, precisely

This repository is a companion tool, not a standalone project. Every rule here is a codified version of a detection command from The React Native Developer's Security Guide — specifically, the rg/grep-based "HOW YOU CATCH IT" block that follows every attack example in the book, and the checklist in the book's Chapter 29 ("Detection: what to grep for"). Nothing here was invented independently of the book; each rule's message explains the attacker technique it targets, and each rule's metadata.book-reference field names the exact chapter that tells the full story — the real incident, the primary source, and why the check works.

If a rule fires and you want the context, the book chapter it cites is where that context lives. This repo is deliberately the part of the book you can point a tool at, not a replacement for reading it.

The book this is extracted from

The React Native Developer's Security Guide is a field guide about the other half of React Native security — not the shipped app and its end users (secure storage, cert pinning, OWASP MASVS, all covered exhaustively elsewhere), but the developer, the workstation, the toolchain, the repository, and the CI system that is an extension of all four. Its argument, in one sentence: an RN repository is a polyglot execution environment disguised as a JavaScript project, and git clone && yarn && pod install && yarn ios runs untrusted, attacker-controllable code in five different runtimes before a line of the app executes.

The book is organized into ten parts and 33 chapters — foundations, the Node/JS layer, config files that are secretly programs, the Apple side, the Android side, the perimeter and the human, what happens on macOS after a compromise, defense, and a chapter on a cross-platform surface (malicious assets) that doesn't fit neatly into "Apple" or "Android" alone. If you haven't read it, the single highest-value page to start with is Appendix A — The File Risk Atlas, a one-page reference table of every file in an RN repo that can execute code and when — it's the mental model this entire rule set is organized around.

Repository structure

rn-dev-security-guide-opengrep/
├── README.md                        this file
├── LICENSE                          MIT
├── deep-scan.semgrepignore          opt-in override to scan node_modules/ too
└── 01-install-scripts.yaml          one file per book chapter (or chapter cluster),
    02-lockfile-integrity.yaml       numbered to roughly track reading order —
    03-js-config-execution.yaml      see the table below for the exact mapping
    ...
    20-malicious-assets.yaml

Every YAML file is a small, self-contained rules: list in standard opengrep/semgrep syntax, with a header comment explaining the file's theme and citing the book chapter(s) it covers. There's no build step, no dependencies to install beyond opengrep itself, and no hidden state — clone the repo and point opengrep at it.


Quick start

# Install opengrep (see https://github.com/opengrep/opengrep for other platforms,
# including a Windows PowerShell installer and prebuilt release binaries):
curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash

# Clone this repo once:
git clone https://github.com/stephanww/rn-dev-security-guide-opengrep.git

# From the root of the RN repo you want to screen:
opengrep scan --config /path/to/rn-dev-security-guide-opengrep .

That's it for a first pass. Everything below is refinement: how to read the output, how to scan your dependency tree too, how to wire it into CI, and where the edges of what a static scanner can do are (they are real edges — see "What this cannot do").


What "finding" means here

A finding is a reason to look, not a verdict. Plenty of legitimate React Native projects have install hooks, husky hooks, ENV[...] reads in a Podfile, and custom Metro transformers. The value of these rules is converting an unbounded "read everything" review into a bounded "read these forty lines" review — not producing a clean/dirty verdict.

Two fields in every rule's metadata help you triage:

  • confidence — HIGH means the pattern is specific enough that a match is rarely benign (a scheme pre-action in a default RN project, a published IOC, an invisible Unicode character). LOW means the rule is intentionally broad and exists to build an inventory for you to read (e.g. "here is every custom Metro transformer hook in this repo").
  • category — security (read this now), best-practice (a hardening control that's absent), or inventory (informational, not a claim of risk).

Severity (ERROR / WARNING / INFO) tracks the same idea and is what most CI integrations key off of.


Running it

Normal scan (what you want most of the time)

cd /path/to/repo-under-audit
opengrep scan --config /path/to/rn-dev-security-guide-opengrep .

Add --json for machine-readable output, --severity ERROR to see only the high-signal stuff first, or -q to suppress the banner.

Scanning a repo someone just handed you

opengrep's scan command only takes local files/folders as its target — there's no opengrep scan https://github.com/.... (--config does accept a git+<url> for fetching a remote rule set, which is a different thing — that's not what you want here.) Clone it first, then scan the checkout:

git clone https://github.com/org/repo.git
cd repo
opengrep scan --config /path/to/rn-dev-security-guide-opengrep .

That's fine for a repo you already trust or are just curious about. If it's a repo from someone you don't fully trust — a recruiter, a bug reporter, a client's first handoff — don't just cd in and scan it: several things in the book's Appendix A execute before you've read a line (submodule hooks during --recurse-submodules, .vscode/tasks.json on folder-open, etc.). Follow Appendix B — the safe-clone runbook instead:

git --version                                          # >= 2.45.1 (CVE-2024-32002)
git -c core.symlinks=false clone --no-checkout <URL> repo && cd repo
git show HEAD:package.json | jq '.scripts'             # peek at the dangerous files
git show HEAD:.vscode/tasks.json 2>/dev/null            # before anything is on disk
git checkout HEAD                                       # now materialize the tree
opengrep scan --config /path/to/rn-dev-security-guide-opengrep .

Run the scan itself after git checkout HEAD but before you open the folder in an editor or run any install command.

Deep dependency scan (the book's Chapter 5 "screen the tree" workflow)

opengrep, like semgrep, ignores node_modules/ by default — always, regardless of any rule's paths.include. That default is correct for everyday scanning. But a handful of rules in this set are specifically designed to audit your dependency tree the way the book's Chapter 5 describes: after an --ignore-scripts install, scan node_modules for transitive install hooks, obfuscation, and invisible Unicode before anything in it has run.

To include node_modules in the scan, pass the bundled deep-scan.semgrepignore (an absolute path is required — opengrep will error on a relative one):

# 1. Fetch the tree WITHOUT executing it (this is the step that matters):
yarn install --immutable --mode=skip-build
# or: pnpm install --frozen-lockfile --ignore-scripts
# or: npm ci --ignore-scripts

# 2. Now scan it, including node_modules:
opengrep scan --config /path/to/rn-dev-security-guide-opengrep \
  --experimental \
  --semgrepignore-filename "$(cd /path/to/rn-dev-security-guide-opengrep && pwd)/deep-scan.semgrepignore" \
  .

This is slower (there are a lot of files in node_modules) and noisier (rules like rn-security-install-hook-present will also fire on every dependency that has a routine node-gyp rebuild build step) — that's expected. It's the same trade-off the book makes in Chapter 5: this is a bounded review of the real dependency graph, not a clean/dirty signal.


What's in each file, and which book chapter it maps to

Every file's header comment cites the exact book chapter(s) it's drawn from. This table is the map; open the file for the full rationale on any individual rule.

File Book chapter(s) What it covers
01-install-scripts.yaml Ch 5, Ch 8 npm/yarn/pnpm lifecycle hooks in your package.json and (in deep-scan mode) transitive dependencies; install hooks that invoke an AI agent CLI (the nx/s1ngularity technique)
02-lockfile-integrity.yaml Ch 9 overrides/resolutions redirecting a dependency; non-registry resolved hosts in package-lock.json; patches/*.patch adding execution primitives
03-js-config-execution.yaml Ch 10 metro.config.js, babel.config.js, app.config.js, eslint.config.*, jest.config.js etc. containing execution primitives, dynamic URL requires, custom Metro transformer/serializer hooks
04-package-manager-plumbing.yaml Ch 11 .yarnrc.yml yarnPath/plugins/registry redirection; .npmrc script-shell/_authToken/ignore-scripts=false; pnpm-workspace.yaml dangerouslyAllowAllBuilds/minimumReleaseAge; .pnpmfile.cjs presence; NODE_OPTIONS preloading; .envrc presence
05-git-attack-surface.yaml Ch 12 .gitattributes filters, .gitmodules submodules, .lfsconfig, husky hook execution primitives, core.hooksPath redirection
06-editor-agent-config.yaml Ch 13 .vscode/tasks.json folderOpen auto-execution, .vscode/settings.json repo-controlled interpreter paths, .devcontainer initializeCommand, MCP server declarations
07-ruby-ios.yaml Ch 14, Ch 17 Gemfile/Fastfile/Appfile execution primitives, non-RubyGems sources, sudo gem/bundle/pod, known-compromised fastlane plugins/gems, hardcoded MATCH_PASSWORD/P12_PASSWORD
08-cocoapods.yaml Ch 15 Podfile execution primitives, :git => pod sources, podspec prepare_command/script_phase, Podfile.lock checksum tracking
09-xcode-build.yaml Ch 16 Xcode scheme pre-actions (near-zero-FP, a clean RN project has none), project.pbxproj shell-script obfuscation and count tracking, Package.swift execution, react-native-xcode.sh integrity
10-signing-material.yaml Ch 17, Ch 24 osascript/keychain-touching code outside your own interactive shell, embedded PEM private keys, EAS Update/CodePush signing mentions, .p12 imported into login.keychain instead of a temporary keychain, create-keychain with no matching delete-keychain
11-gradle-config.yaml Ch 18 build.gradle/settings.gradle execution primitives (fires at sync, not just build), apply from: <url>, insecure repo URLs, buildSrc, ~/.gradle/init.d references
12-gradle-wrapper.yaml Ch 19 gradle-wrapper.properties distributionUrl/missing checksum, suspicious primitives in gradlew, missing wrapper-validation CI step
13-android-secrets.yaml Ch 20 Keystore passwords in gradle.properties/local.properties, keystore file references, google-services.json server keys, Play service-account JSON, .gitignore gaps
14-dev-server-binding.yaml Ch 21 Metro/RN CLI scripts missing --host 127.0.0.1 (CVE-2025-11953 context), cli-server-api version inventory
15-ci-workflows.yaml Ch 23 Actions not pinned to a commit SHA, tj-actions/reviewdog IOCs, pull_request_target + fork checkout, workflow_run + artifact consumption, missing permissions:, self-hosted runners
16-invisible-unicode.yaml Ch 7, Ch 13, Ch 29 The single highest-value check in this set. Zero-width/bidi characters anywhere (near-zero FP); variation-selector runs (the GlassWorm steganography signature); both, with a dedicated rule for AI agent instruction files
17-obfuscation.yaml Ch 22, Ch 29 eval/Function/atob combined with hex-mangled identifiers, large base64 blobs, very long lines, a generic env-var-read + outbound-request combination
18-known-iocs.yaml Ch 5, Ch 6, Ch 7, Ch 8, Ch 14, Ch 15, Ch 24 Published indicators from named, dated 2025–2026 incidents: the March 2026 RN package compromise (package names, C2 IP, Solana wallet), Shai-Hulud, chalk/debug, SleeperGem, s1ngularity, AMOS staging artifacts
19-hardening-config-present.yaml Ch 27, Ch 31 Positive assertions — flags the absence of ignore-scripts, min-release-age/minimumReleaseAge/npmMinimalAgeGate, a hash-pinned packageManager, and CODEOWNERS coverage of executable-config paths. This is the literal "CI check that your hardening is still present" the book recommends, so a control removed under deadline pressure fails loudly instead of silently.
20-malicious-assets.yaml Ch 32 SVG files containing <script>/event handlers/javascript: URIs; build scripts shelling out to ImageMagick or JS image-processing libraries with their own CVE history

The one check to run today, everywhere, if you do nothing else

opengrep scan --config /path/to/rn-dev-security-guide-opengrep/16-invisible-unicode.yaml .

Zero-width characters, bidi text-direction overrides, and Unicode variation selectors do not render. GlassWorm (a self-propagating OpenVSX/VS Code extension worm, October 2025) and the "Rules File Backdoor" (hidden instructions to AI coding agents, disclosed March 2025) both defeat human code review this way — a reviewer looking directly at the malicious line sees blank space, in the editor and in the GitHub PR diff. No human reviewer can catch this by reading the screen; only byte-level tooling can. It's one file, it runs in well under a second, and it closes an entire attack class. Put it in CI first, before anything else here.


Wiring this into CI

# .github/workflows/security.yml
name: security-checks
on: [push, pull_request]
jobs:
  opengrep:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Checkout rule set
        uses: actions/checkout@v4
        with:
          repository: stephanww/rn-dev-security-guide-opengrep
          path: opengrep-rules
      - name: Install opengrep
        run: curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash
      - name: Run rn-security rules
        run: opengrep scan --config opengrep-rules --error --severity ERROR .

--error makes opengrep exit non-zero on any finding at or above the given --severity, which fails the build. Starting with --severity ERROR only (the high-confidence rules — invisible Unicode, scheme pre-actions, known IOCs, .npmrc token leaks, pull_request_target + fork checkout, etc.) keeps the initial rollout noise-free; add WARNING once the team is used to triaging findings. See 19-hardening-config-present.yaml for the config-presence checks the book's Chapter 31 recommends specifically so a hardening setting that gets removed under deadline pressure is a visible, named CI failure rather than a silent regression six months later.

If you want this to run before yarn/npm install, wire it as a pre-commit or pre-push hook instead — but see the caveat in Appendix A about .husky/* itself being repo-controlled: keep your CI copy of this rule set independent of what a compromised repo could edit. Pinning the actions/checkout step above to a commit SHA rather than a branch avoids exactly the mutable-reference problem the book's Chapter 23 describes.


What this rule set deliberately does NOT do

Static text/AST matching has real limits, and the book is explicit about them (Chapter 29, "What detection will not do"). Specifically:

  • It will not catch a competent, targeted attacker. Bespoke malware written specifically for you has no signature. Detection is the third line of defense — prevention (book Ch 27) and isolation (book Ch 28) are where the real leverage is, and no rule file substitutes for running an unknown repository in a disposable VM — the one piece of advice in the book its author calls non-negotiable.
  • It cannot hash-verify binaries. gradle-wrapper.jar is a compiled JAR; opengrep can check the surrounding .properties file and flag a missing checksum, but the actual JAR integrity check is gradle/actions/wrapper-validation@v4 in CI (mentioned in 12-gradle-wrapper.yaml's info-level rule) or a manual shasum -a 256 comparison.
  • It cannot check machine state. Persistence (~/Library/LaunchAgents, shell rc files, ~/.gradle/init.d), keychain contents, listening ports, and login items are outside a source-tree scanner's reach entirely. Run the book's Appendix C — macOS triage one-liners or the preflight.sh script from its Chapter 29 for that layer, and keep your copy of that script outside the repo you're auditing.
  • It does not replace npm audit / osv-scanner / gitleaks / TruffleHog. Those find known CVEs and known-pattern secrets respectively; this rule set finds structural and behavioral risk (execution primitives in files that shouldn't have them, redirection points, published incident IOCs). Run all of them — the book's Chapter 29 has the full toolbox list.

A note on how these rules were tuned

Every rule was validated with opengrep validate (0 fatal errors across all 119 rules) and then run against a purpose-built fixture tree reproducing the book's own defanged attack examples (a malicious postinstall, a metro.config.js with a disguised credential-recon function, a Podfile post_install block, a scheme pre-action, invisible Unicode in an agent-rules file, and so on) to confirm true positives fire.

The rules were then run against four real, unrelated production React Native codebases and iterated until the obvious noise was gone. This mattered — the first pass produced several false-positive patterns that only show up against real code, not synthetic fixtures, and the fix for each is recorded in that rule's message so you understand why something isn't flagged, not just that it isn't:

  • Emoji variation selectors (console.log('⚠️ ...')) matched the invisible-Unicode check on nearly every real codebase tested. Fixed by requiring a run of 2+ consecutive variation selectors — the actual GlassWorm steganographic signature — instead of a single one, which is normal emoji rendering.
  • nc\s inside "sync" (a bare, unanchored substring match for the nc command) matched the word "sync" in a completely unrelated comment inside a stock CocoaPods build phase. Fixed with a word boundary.
  • :path => in ios/Podfile matched on every single React Native native-module link (pod 'X', :path => '../node_modules/...' is standard autolinking boilerplate), drowning out the real signal (:git =>, which actually bypasses the registry). Split into two rules.
  • require($URL) as a semgrep pattern matched any require() call with any argument, not just URLs — a metavariable with no constraint matches anything. Fixed with metavariable-regex.
  • ENV[...] in a Podfile/Fastfile/Gemfile is extremely common and legitimate (ENV['USE_FRAMEWORKS'], ENV['CI']) — downgraded to a separate low-severity inventory rule, kept distinct from the high-signal system/backtick/IO.popen execution primitives.
  • eval in android/gradlew — the official, unmodified Gradle wrapper script legitimately contains eval as part of its argument-quoting logic. Removed from the "suspicious primitive" pattern; the network/exfiltration primitives (curl, wget, base64, osascript, etc.) remain.
  • Base64 blobs and very-long-lines fired constantly on SVG assets and mock fixture data (base64-encoded sample images in test fixtures). Excluded **/*.svg, **/mock/**, **/fixtures/**, and test files from both checks.
  • A handful of rules stayed intentionally noisy — e.g. rn-security-keystore-password-in-repo and rn-security-google-services-server-key found real plaintext keystore passwords and API keys across the test codebases. That is the rule working, not a bug to fix.

If a rule fires on something you're confident is benign in your codebase, prefer adding a path exclusion in that rule's paths.exclude (with a comment explaining why) over lowering its severity or deleting it — the next person's fork of your config should get the same protection you tuned in.


Extending this rule set

Each rule file is a small, self-contained rules: list — normal opengrep/semgrep YAML. To add a rule:

  1. Find (or create) the file matching the book chapter it belongs to.
  2. Follow the existing pattern: id prefixed rn-security-, a message that explains the attacker technique (not just "this looks risky"), a book-reference in metadata linking back to the specific chapter, and a confidence/category pair.
  3. Validate: opengrep validate <your-file>.yaml
  4. Test it against something that should trip it and something that shouldn't, the way the book's Chapter 29 insists on — "test your detection by running it against something you know is bad… and if you never test it, you have a green check mark and no idea what it means."

If the rule reflects a gap or correction in the book itself — a new technique, a false positive, a case the book's narrative should have covered — open the issue against the book repo too. The two projects move together: a narrative gap gets fixed in the chapter first, then the rule follows, per the book README's "how reader feedback becomes book content" section.


Further reading


License

MIT. The companion book is licensed separately — see its repository for details (Creative Commons Attribution 4.0 International).

About

opengrep rules that help detect supply-chain and toolchain attacks against React Native developers on macOS — the companion rule set for the RN Developer's Security Guide.

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors