diff --git a/.github/labeler.yml b/.github/labeler.yml index 318d2e824f7..ecc6c9cbca4 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -238,8 +238,11 @@ "security": - changed-files: - any-glob-to-any-file: + - ".github/workflows/opengrep-*.yml" + - ".semgrepignore" - "docs/cli/security.md" - "docs/gateway/security.md" + - "security/**" "extensions: copilot-proxy": - changed-files: diff --git a/.github/workflows/opengrep-precise-full.yml b/.github/workflows/opengrep-precise-full.yml new file mode 100644 index 00000000000..834f4135e7f --- /dev/null +++ b/.github/workflows/opengrep-precise-full.yml @@ -0,0 +1,67 @@ +name: OpenGrep — Full + +# Manual repository-wide scan for the high-precision OpenGrep rule super-config. +# This is intentionally separate from PR scanning so broad/backlog findings do +# not block unrelated pull requests. + +on: + workflow_dispatch: + +concurrency: + group: opengrep-full-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + security-events: write + +jobs: + scan: + name: Scan full repository (precise) + runs-on: blacksmith-16vcpu-ubuntu-2404 + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install opengrep + env: + # Pin both the install script (by commit SHA) and the binary version. + # The script SHA must match the v1.19.0 release tag in opengrep/opengrep + # so a compromised or force-pushed `main` cannot RCE in our CI runner. + # Bump both together when upgrading. + OPENGREP_VERSION: v1.19.0 + OPENGREP_INSTALL_SHA: 9a4c0a68220618441608cd2bad4ff2eddccf8113 + run: | + curl -fsSL "https://raw.githubusercontent.com/opengrep/opengrep/${OPENGREP_INSTALL_SHA}/install.sh" \ + | bash -s -- -v "$OPENGREP_VERSION" + echo "$HOME/.opengrep/cli/latest" >> "$GITHUB_PATH" + + - name: Verify opengrep + run: opengrep --version + + - name: Run full opengrep scan + # Manual full scans cover all first-party source paths so maintainers can + # audit the complete rulepack without making PRs inherit unrelated backlog. + run: | + mkdir -p .opengrep-out + scripts/run-opengrep.sh --sarif --error + + - name: Upload SARIF to GitHub Code Scanning + uses: github/codeql-action/upload-sarif@v3 + # Only upload if the scan actually produced a SARIF file. + if: always() && hashFiles('.opengrep-out/precise.sarif') != '' + with: + sarif_file: .opengrep-out/precise.sarif + category: opengrep-full + + - name: Upload SARIF as workflow artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: opengrep-full-sarif + path: .opengrep-out/precise.sarif + if-no-files-found: warn + retention-days: 30 diff --git a/.github/workflows/opengrep-precise.yml b/.github/workflows/opengrep-precise.yml new file mode 100644 index 00000000000..5ee59c23258 --- /dev/null +++ b/.github/workflows/opengrep-precise.yml @@ -0,0 +1,76 @@ +name: OpenGrep — PR Diff + +# Runs the high-precision OpenGrep rule super-config against only first-party +# source paths changed by a pull request. Keeping PR scans diff-scoped makes +# findings attributable to the proposed change instead of surfacing unrelated +# repository-wide backlog. +# +# For a repository-wide scan, use the manual OpenGrep — Full workflow. + +on: + pull_request: + +concurrency: + group: opengrep-pr-diff-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +permissions: + contents: read + security-events: write + +jobs: + scan: + name: Scan changed paths (precise) + runs-on: blacksmith-16vcpu-ubuntu-2404 + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + # `scripts/run-opengrep.sh --changed` diffs base...HEAD. + fetch-depth: 0 + + - name: Install opengrep + env: + # Pin both the install script (by commit SHA) and the binary version. + # The script SHA must match the v1.19.0 release tag in opengrep/opengrep + # so a compromised or force-pushed `main` cannot RCE in our CI runner. + # Bump both together when upgrading. + OPENGREP_VERSION: v1.19.0 + OPENGREP_INSTALL_SHA: 9a4c0a68220618441608cd2bad4ff2eddccf8113 + run: | + curl -fsSL "https://raw.githubusercontent.com/opengrep/opengrep/${OPENGREP_INSTALL_SHA}/install.sh" \ + | bash -s -- -v "$OPENGREP_VERSION" + echo "$HOME/.opengrep/cli/latest" >> "$GITHUB_PATH" + + - name: Verify opengrep + run: opengrep --version + + - name: Run opengrep on PR diff + env: + OPENCLAW_OPENGREP_BASE_REF: ${{ github.event.pull_request.base.sha }}...HEAD + # Findings from precise rules block this workflow. Pull requests scan + # changed first-party source paths only so findings stay attributable to + # the PR diff. Test/fixture/QA path exclusions live in `.semgrepignore` + # at the repo root and are picked up automatically. + run: | + mkdir -p .opengrep-out + scripts/run-opengrep.sh --changed --sarif --error + + - name: Upload SARIF to GitHub Code Scanning + uses: github/codeql-action/upload-sarif@v3 + # Only upload if the scan actually produced a SARIF file. + if: always() && hashFiles('.opengrep-out/precise.sarif') != '' + with: + sarif_file: .opengrep-out/precise.sarif + category: opengrep-pr-diff + + - name: Upload SARIF as workflow artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: opengrep-pr-diff-sarif + path: .opengrep-out/precise.sarif + if-no-files-found: warn + retention-days: 30 diff --git a/.gitignore b/.gitignore index 238820de385..f9bf44e1761 100644 --- a/.gitignore +++ b/.gitignore @@ -191,3 +191,6 @@ extensions/qa-lab/web/dist/ # Generated bundled plugin runtime dependency manifests extensions/**/.openclaw-runtime-deps.json extensions/**/.openclaw-runtime-deps-stamp.json + +# Output dir for scripts/run-opengrep.sh (local opengrep scans) +/.opengrep-out/ diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 00000000000..d49b1f276c3 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,95 @@ +# .semgrepignore — single source of truth for paths excluded from +# opengrep / semgrep scans run against this repo. +# +# Syntax: gitignore-style globs (https://git-scm.com/docs/gitignore). +# Consumed automatically by `opengrep scan` and `semgrep scan`. The compiled +# detector rulepacks under security/opengrep/ and the GitHub Actions workflows +# under .github/workflows/opengrep-*.yml all rely on this file rather than +# duplicating exclude lists in 50+ places. +# +# When adding a new test naming convention, fixture directory, or QA-tooling +# extension to the codebase, add its glob here so the security rulepacks +# stop firing on it. Real product code should never match anything in this +# file. + +# ---------------------------------------------------------------------------- +# Standard test file suffixes +# ---------------------------------------------------------------------------- +*.test.* +*.spec.* + +# ---------------------------------------------------------------------------- +# Fixture & mock file suffixes (cover both .foo and -foo styles used in repo) +# ---------------------------------------------------------------------------- +*.fixture.* +*-fixture.* +*-fixtures.* +*.mock.* +*-mock.* +*-mocks.* + +# ---------------------------------------------------------------------------- +# Test helper / harness / support / shared / utils naming conventions +# ---------------------------------------------------------------------------- +*.test-helper.* +*.test-helpers.* +*-test-helpers.* +*.test-harness.* +*-test-harness.* +*.test-support.* +*-test-support.* +*.test-shared.* +*-test-shared.* +*.test-mocks.* +*-test-mocks.* +*.test-utils.* +*-test-utils.* +*.test-fixtures.* +*-test-fixtures.* +*.e2e-test-helpers.* + +# Bare top-of-dir test helper files (e.g. extensions/foo/src/test-helpers.ts) +test-helper.* +test-helpers.* +test-harness.* +test-support.* +test-shared.* +test-utils.* +test-mocks.* +test-fixtures.* +test-fetch.* +test-manager-helpers.* + +# ---------------------------------------------------------------------------- +# Test / mock / fixture directories anywhere in the tree +# ---------------------------------------------------------------------------- +__tests__/ +__mocks__/ +test/ +tests/ +test-fixtures/ +test-fixture/ +test-helpers/ +test-utils/ +test-support/ +test-mocks/ +test-harness/ +fixtures/ +mocks/ + +# ---------------------------------------------------------------------------- +# QA tooling — entire QA-only directories and extensions, not product code +# ---------------------------------------------------------------------------- +qa/ +qa-lab/ +extensions/qa-*/ + +# ---------------------------------------------------------------------------- +# Top-level scripts that drive tests rather than ship product behavior +# ---------------------------------------------------------------------------- +scripts/test-* +scripts/run-vitest* +scripts/run-tests* +scripts/lib/test-* +scripts/lib/extension-test-* +scripts/lib/vitest-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 771e51305ba..5a36c654886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Gateway/memory: add a read-only `doctor.memory.remHarness` RPC so operator clients can preview bounded REM dreaming output without running mutation paths. (#66673) Thanks @samzong. - Gateway/events: surface `spawnedBy` on subagent chat and agent broadcast payloads so clients can route child session events without an extra session lookup. (#63244) Thanks @samzong. - Security policy: classify media/base64 decode and format-conversion overhead after configured acceptance limits as performance-only for GHSA triage unless a report demonstrates a limit bypass, crash, exhaustion, data exposure, or another boundary bypass. (#74311) +- Security/OpenGrep: add a precise OpenGrep rulepack, source-rule compiler, provenance metadata check, and PR/full scan workflows that validate first-party code and rulepack-only changes while uploading SARIF to GitHub Code Scanning. (#69483) Thanks @jesse-merhi. ### Fixes diff --git a/package.json b/package.json index fdad0fbed59..368ebc2e8fc 100644 --- a/package.json +++ b/package.json @@ -1281,6 +1281,7 @@ "check:madge-import-cycles": "node --import tsx scripts/check-madge-import-cycles.ts", "check:no-conflict-markers": "node scripts/check-no-conflict-markers.mjs", "check:no-runtime-action-load-config": "node scripts/check-no-runtime-action-load-config.mjs", + "check:opengrep-rule-metadata": "node security/opengrep/check-rule-metadata.mjs", "check:runtime-sidecar-loaders": "node --import tsx scripts/check-runtime-sidecar-loaders.mjs", "check:static-import-sccs": "pnpm check:madge-import-cycles", "check:temp-path-guardrails": "node --import tsx scripts/check-temp-path-guardrails.ts", diff --git a/scripts/check.mjs b/scripts/check.mjs index c143e336c63..7bfe65e06d2 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -42,6 +42,7 @@ export async function main(argv = process.argv.slice(2)) { { name: "runtime sidecar loader guard", args: ["check:runtime-sidecar-loaders"] }, { name: "tool display", args: ["tool-display:check"] }, { name: "host env policy", args: ["check:host-env-policy:swift"] }, + { name: "opengrep rule metadata", args: ["check:opengrep-rule-metadata"] }, ], }, { diff --git a/scripts/run-opengrep.sh b/scripts/run-opengrep.sh new file mode 100755 index 00000000000..7300afbe559 --- /dev/null +++ b/scripts/run-opengrep.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# scripts/run-opengrep.sh +# +# Run the OpenClaw precise OpenGrep rulepack against the local working tree +# using the same paths and exclusions as CI. The .semgrepignore at the repo root +# is the single source of truth for skipped paths. +# +# Usage: +# scripts/run-opengrep.sh # precise, human output +# scripts/run-opengrep.sh precise # same +# scripts/run-opengrep.sh --sarif # write SARIF for upload/triage +# scripts/run-opengrep.sh --json # write JSON for ad-hoc parsing +# scripts/run-opengrep.sh --changed # scan changed first-party paths +# scripts/run-opengrep.sh --error # fail non-zero on findings +# +# Optional positional path overrides come last: +# scripts/run-opengrep.sh -- src/agents/ # scan a single dir +# +# Exit code: non-zero on scan errors, and on findings when --error is passed. + +set -euo pipefail + +BUCKET="precise" +if [[ "${1:-}" == "precise" ]]; then + shift +elif [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + sed -n '2,22p' "$0" + exit 0 +elif [[ "${1:-}" == "broad" ]]; then + echo "error: broad OpenGrep rulepacks are not supported in this repo workflow" >&2 + exit 64 +fi + +# Resolve repo root from this script's location so the command works from any cwd. +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CONFIG="$REPO_ROOT/security/opengrep/precise.yml" + +if [[ ! -f "$CONFIG" ]]; then + echo "error: rulepack not found at $CONFIG" >&2 + echo "Recompile with: node security/opengrep/compile-rules.mjs --rules-dir --out-dir security/opengrep" >&2 + exit 66 +fi + +if ! command -v opengrep >/dev/null 2>&1; then + cat >&2 <<'EOF' +error: 'opengrep' not found on PATH. + +Install with one of: + curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/v1.19.0/install.sh | bash -s -- -v v1.19.0 + brew install opengrep/tap/opengrep + pipx install opengrep + +(See https://opengrep.dev for other options.) +EOF + exit 127 +fi + +# Pull off our own flags from the remaining args; pass everything else through to opengrep. +EXTRA_ARGS=() +PATHS_PASSED=0 +SAW_DOUBLE_DASH=0 +CHANGED_ONLY=0 +FAIL_ON_FINDINGS=0 +while (( $# > 0 )); do + case "$1" in + --sarif) + mkdir -p "$REPO_ROOT/.opengrep-out" + EXTRA_ARGS+=( "--sarif-output=$REPO_ROOT/.opengrep-out/$BUCKET.sarif" ) + shift + ;; + --json) + mkdir -p "$REPO_ROOT/.opengrep-out" + EXTRA_ARGS+=( "--json" "--output=$REPO_ROOT/.opengrep-out/$BUCKET.json" ) + shift + ;; + --changed) + CHANGED_ONLY=1 + shift + ;; + --error) + FAIL_ON_FINDINGS=1 + shift + ;; + --) + SAW_DOUBLE_DASH=1 + shift + ;; + *) + if (( SAW_DOUBLE_DASH )); then + # Treat anything after `--` as a path-positional override + if (( PATHS_PASSED == 0 )); then + PATHS_PASSED=1 + EXTRA_ARGS+=( "$1" ) + else + EXTRA_ARGS+=( "$1" ) + fi + else + EXTRA_ARGS+=( "$1" ) + fi + shift + ;; + esac +done + +cd "$REPO_ROOT" + +if (( CHANGED_ONLY && PATHS_PASSED )); then + echo "error: --changed cannot be combined with explicit path overrides" >&2 + exit 64 +fi + +# Default scan paths match CI. Override by passing `-- `. +if (( PATHS_PASSED == 0 )); then + if (( CHANGED_ONLY )); then + mapfile -t SCAN_PATHS < <( + { + git diff --name-only --diff-filter=ACMRTUXB "${OPENCLAW_OPENGREP_BASE_REF:-origin/main...HEAD}" 2>/dev/null || true + git diff --name-only --diff-filter=ACMRTUXB -- 2>/dev/null || true + git ls-files --others --exclude-standard + } | awk '/^(src|extensions|apps|packages|scripts)\// { print }' | sort -u + ) + mapfile -t RULEPACK_CHANGED_PATHS < <( + { + git diff --name-only --diff-filter=ACMRTUXB "${OPENCLAW_OPENGREP_BASE_REF:-origin/main...HEAD}" 2>/dev/null || true + git diff --name-only --diff-filter=ACMRTUXB -- 2>/dev/null || true + git ls-files --others --exclude-standard + } | awk '/^(security\/opengrep\/|scripts\/run-opengrep\.sh$|\.semgrepignore$|\.github\/workflows\/opengrep-)/ { print }' | sort -u + ) + if (( ${#SCAN_PATHS[@]} == 0 && ${#RULEPACK_CHANGED_PATHS[@]} > 0 )); then + SCAN_PATHS=( "security/opengrep/precise.yml" ) + fi + if (( ${#SCAN_PATHS[@]} == 0 )); then + echo "→ No changed first-party paths for opengrep." >&2 + exit 0 + fi + else + SCAN_PATHS=( "src/" "extensions/" "apps/" "packages/" "scripts/" ) + fi +else + SCAN_PATHS=() +fi + +if (( FAIL_ON_FINDINGS )); then + EXTRA_ARGS+=( "--error" ) +fi + +echo "→ Running opengrep ($BUCKET) against $(IFS=' '; echo "${SCAN_PATHS[*]:-overridden}")" >&2 +echo " Using exclusions from .semgrepignore" >&2 +exec opengrep scan \ + --no-strict \ + --config "$CONFIG" \ + --no-git-ignore \ + "${EXTRA_ARGS[@]}" \ + "${SCAN_PATHS[@]}" diff --git a/security/README.md b/security/README.md new file mode 100644 index 00000000000..2747ce131c4 --- /dev/null +++ b/security/README.md @@ -0,0 +1,136 @@ +# Security tooling + +This directory holds OpenClaw's shipped OpenGrep security rulepack and the +supporting tooling that validates and runs it. Maintainer-only advisory triage +and detector-generation prompts live outside the public repo; this repo keeps the +durable artifacts needed to block regressions in PRs and support local rule +validation. + +## Layout + +```text +security/ +├── README.md <- this file +└── opengrep/ + ├── README.md <- precise rulepack details + compile recipe + └── precise.yml <- compiled super-config: precise rules +``` + +The related scripts are: + +- `security/opengrep/compile-rules.mjs` — gathers source OpenGrep rule YAMLs from + a folder and appends new compiled rule IDs to `security/opengrep/precise.yml`. +- `security/opengrep/check-rule-metadata.mjs` — enforces that every committed + rule carries durable source/provenance metadata. +- `scripts/run-opengrep.sh` — runs the compiled precise rulepack locally or in + CI with consistent paths and exclusions. + +## Rule lifecycle + +Maintainers investigate advisories and generate candidate rules outside the public repo. +Once a candidate rule has been validated and reviewed, put the shippable source +rule YAML in any local folder and compile it into this repo: + +```bash +node security/opengrep/compile-rules.mjs \ + --rules-dir +``` + +Commit the resulting `security/opengrep/precise.yml` diff. Durable rule +provenance lives in each compiled rule's metadata and is checked by +`pnpm check:opengrep-rule-metadata`. + +Rule quality contract: precise rules must catch the vulnerable behavior they were +written for, should be silent on corresponding fixed behavior when a fix exists, +and should keep current findings limited to verified regressions or variants. + +## Writing precise OpenGrep rules + +A rule is appropriate for `security/opengrep/precise.yml` only when the dangerous +shape is stable enough to block PRs. Prefer, in order: + +1. **Variant detector** — source-to-sink or missing-guard detection across the + same bug family. +2. **Scoped behavioral regression** — a narrow subsystem-specific rule anchored + on the affected API or trust boundary. +3. **Exact regression canary** — a labelled canary for the original vulnerable + shape when broader variants would be noisy. +4. **No OpenGrep rule** — if runtime state, product policy, or external data is + required to distinguish vulnerable and safe behavior. + +Before compiling a rule, validate it against vulnerable/fixed/current code when +those surfaces exist. Every current finding must be classified as a true original +issue or true variant, or the rule must be tightened/dropped before it ships. + +## Running the rules locally + +The wrapper script handles paths, exclusions, and output formatting so local +scans match CI exactly. + +```bash +scripts/run-opengrep.sh # precise rules, human output +scripts/run-opengrep.sh --json # write .opengrep-out/precise.json +scripts/run-opengrep.sh --sarif # write .opengrep-out/precise.sarif +scripts/run-opengrep.sh --changed # scan changed first-party paths +scripts/run-opengrep.sh -- src/agents/ # scan a single dir +``` + +If you'd rather invoke `opengrep` directly, the equivalent is: + +```bash +opengrep scan --no-strict --no-git-ignore \ + --config security/opengrep/precise.yml \ + src/ extensions/ apps/ packages/ scripts/ +``` + +Both forms read `.semgrepignore` at the repo root automatically — that's the +single source of truth for which paths are skipped (test files, fixtures, mocks, +QA-tooling extensions, test-orchestration scripts, …). Add a glob there if a new +test naming convention shows up. + +## Running the rules in CI + +There are two OpenGrep workflows: + +- **OpenGrep — PR Diff** (`.github/workflows/opengrep-precise.yml`) runs on pull + requests and executes `scripts/run-opengrep.sh --changed --sarif --error` so + findings stay scoped to changed first-party paths. +- **OpenGrep — Full** (`.github/workflows/opengrep-precise-full.yml`) is manual + dispatch only and executes `scripts/run-opengrep.sh --sarif --error` across + the full first-party source set for maintainers who want a repository-wide + audit. + +Both workflows: + +- Inherit the same `.semgrepignore` exclusions used by the local wrapper +- Upload SARIF to GitHub Code Scanning under stable OpenGrep categories +- Fail on precise findings so the rulepack acts as a regression firewall +- Enforce committed rule provenance with `pnpm check:opengrep-rule-metadata` + +## Editing, silencing, or removing rules + +`precise.yml` is the checked-in compiled rulepack. Prefer editing source rule +YAML and recompiling instead of hand-editing compiled rules, because the compiler +normalizes rule IDs, metadata, duplicates, and OpenGrep validation. The compiler +appends new rule IDs by default; use `--replace-precise` only when intentionally +rebuilding the rulepack from a complete source folder. + +To drop a noisy rule: + +1. Delete the offending source rule from the local source-rule folder. +2. Re-run `node security/opengrep/compile-rules.mjs --rules-dir `. +3. Commit the resulting `security/opengrep/precise.yml` diff. + +To narrow a rule's path scope, edit the source rule's `paths.include` / +`paths.exclude` fields in the same local artifact location and recompile. + +## Tracing a finding back to its source + +Every compiled rule's `id` is `.`. For GHSA-backed rules, +`` is the lower-case GHSA ID. For other source-backed rules, use a +stable source identifier without dots such as a CVE, OSV ID, internal advisory ID, or other +review identifier. Rule `metadata` must include `advisory-url`, +`detector-bucket`, and `source-rule-id`, plus either `ghsa` or `advisory-id`. +New compilations also add `source-file` when available. +`pnpm check:opengrep-rule-metadata` enforces these durable source fields so each +committed rule is traceable without a separate committed manifest. diff --git a/security/opengrep/README.md b/security/opengrep/README.md new file mode 100644 index 00000000000..091161574a1 --- /dev/null +++ b/security/opengrep/README.md @@ -0,0 +1,103 @@ +# Compiled OpenGrep super-configs + +`precise.yml` is OpenClaw's shipped precise OpenGrep rulepack. Each rule is tied +to a source advisory, vulnerability report, or review identifier through metadata +and is intended to have concrete coverage of the original vulnerable behavior or +a verified variant. + +Rule provenance lives in each compiled rule's metadata; no separate manifest is +committed or generated by default. + +Noisy exploratory rules are intentionally kept out of the tracked repo. Anything +appended to `precise.yml` must be low-noise enough to run as a blocking PR-diff +check and as a manual full-repository audit. + +## Editing rules + +`precise.yml` is the checked-in compiled rulepack. Prefer changing source rule +YAML and rerunning `security/opengrep/compile-rules.mjs` instead of hand-editing +compiled rules. The compiler appends new rule IDs by default; use +`--replace-precise` only when intentionally rebuilding the rulepack from a +complete source folder. Direct edits are discouraged because they can bypass ID, +metadata, duplicate, and OpenGrep validation. + +## Rule naming and metadata + +Every rule's id is rewritten to `.`. Every rule's +`metadata` block is augmented with source fields enforced by +`pnpm check:opengrep-rule-metadata`: + +| Key | Value | +| ----------------- | --------------------------------------------------------------------- | +| `ghsa` | `GHSA-xxxx-xxxx-xxxx` for GHSA-backed rules | +| `advisory-id` | non-GHSA source identifier, or the GHSA ID normalized by the compiler | +| `advisory-url` | durable URL to the advisory, report, review record, or source context | +| `detector-bucket` | `precise` | +| `source-rule-id` | the original source rule id | +| `source-file` | optional source YAML file used during compilation | + +## Recompiling + +```bash +# from the openclaw repo root +node security/opengrep/compile-rules.mjs \ + --rules-dir +``` + +The script: + +1. Recursively walks every `.yml` / `.yaml` file under `--rules-dir` +2. Reads top-level `rules` arrays from those source files +3. Requires each source rule to provide `metadata.ghsa` or `metadata.advisory-id` +4. Requires `metadata.advisory-url` for non-GHSA source identifiers +5. Rewrites ids and injects metadata as above +6. Appends only new precise rule ids to the existing `precise.yml` by default; pass `--replace-precise` to rebuild it from just the supplied source folder +7. Runs `opengrep scan --no-strict` against an empty target to identify schema-invalid or parser-invalid rules and drops mapped bad rules so the published super-config loads cleanly +8. Writes `precise.yml` + +Skipped, duplicate, or invalid rules are summarized on stdout/stderr for follow-up. + +## Validating locally + +```bash +pnpm check:opengrep-rule-metadata +opengrep validate security/opengrep/precise.yml +``` + +The metadata check must pass before rules are committed. OpenGrep validation must +exit zero. Warnings about unknown fields are acceptable only when OpenGrep still +reports `Configuration is valid` and a non-zero rule count. The compile script +drops mapped schema/parser-invalid rules and fails closed when OpenGrep +validation itself cannot be completed. + +## Running locally + +```bash +scripts/run-opengrep.sh +``` + +For SARIF output matching the PR workflow's diff-scoped scan: + +```bash +scripts/run-opengrep.sh --changed --sarif +``` + +For SARIF output matching the manual full-repository workflow: + +```bash +scripts/run-opengrep.sh --sarif +``` + +## Why `--no-strict`? + +Some generated rules trigger non-fatal opengrep warnings (for example, +unknown-field warnings on compatibility-only keys). `--no-strict` keeps +opengrep's exit code clean for those warnings. Parser-invalid rules are still +dropped during compilation so the checked-in super-config validates before CI +uses it. + +## Why `--no-git-ignore`? + +Some OpenClaw paths are excluded by `.gitignore` for build reasons even though +they contain meaningful source code we want scanned. `--no-git-ignore` keeps +opengrep from skipping them. diff --git a/security/opengrep/check-rule-metadata.mjs b/security/opengrep/check-rule-metadata.mjs new file mode 100644 index 00000000000..d7df9fb6863 --- /dev/null +++ b/security/opengrep/check-rule-metadata.mjs @@ -0,0 +1,138 @@ +#!/usr/bin/env node +import { promises as fs } from "node:fs"; +import * as path from "node:path"; +import { parseDocument } from "yaml"; + +const DEFAULT_RULEPACK = path.resolve("security", "opengrep", "precise.yml"); +const GHSA_RE = /^GHSA-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}$/; +const RULE_ID_RE = /^([a-z0-9][a-z0-9_-]*)\..+$/; + +function printHelp() { + console.log(`Usage: node security/opengrep/check-rule-metadata.mjs [rulepack.yml] + +Checks that every compiled OpenGrep rule carries source/provenance metadata. +Default rulepack: ${DEFAULT_RULEPACK} +`); +} + +export async function readRules(rulepackPath) { + const raw = await fs.readFile(rulepackPath, "utf8"); + const doc = parseDocument(raw, { keepSourceTokens: false }); + if (doc.errors.length > 0) { + throw new Error( + `Could not parse ${rulepackPath}: ${doc.errors.map((e) => e.message).join("; ")}`, + ); + } + const data = doc.toJSON(); + if (!data || !Array.isArray(data.rules)) { + throw new Error(`${rulepackPath} must contain a top-level rules array`); + } + return data.rules; +} + +function hasNonEmptyString(value) { + return typeof value === "string" && value.trim().length > 0; +} + +function sanitizeIdComponent(value) { + return ( + String(value || "") + .replace(/[^a-zA-Z0-9._-]+/g, "-") + .replace(/^-+|-+$/g, "") + .toLowerCase() || "rule" + ); +} + +function sanitizeSourceIdComponent(value) { + return sanitizeIdComponent(value).replace(/[.]+/g, "-"); +} + +export function validateRuleMetadata(rules) { + const violations = []; + + for (const [index, rule] of rules.entries()) { + const id = String(rule?.id ?? ""); + const label = id || `rules[${index}]`; + const metadata = rule?.metadata; + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { + violations.push(`${label}: missing metadata object`); + continue; + } + + const idMatch = id.match(RULE_ID_RE); + if (!idMatch) { + violations.push(`${label}: id must match .`); + } + + const ghsa = String(metadata.ghsa ?? ""); + const advisoryId = String(metadata["advisory-id"] ?? metadata.ghsa ?? "") + .trim() + .toUpperCase(); + if (!hasNonEmptyString(advisoryId)) { + violations.push(`${label}: missing metadata.advisory-id or metadata.ghsa`); + } else if (idMatch && idMatch[1] !== sanitizeSourceIdComponent(advisoryId)) { + violations.push( + `${label}: source id in metadata (${advisoryId}) must match source id in rule id (${idMatch[1]})`, + ); + } + + if (ghsa && !GHSA_RE.test(ghsa)) { + violations.push(`${label}: metadata.ghsa must match GHSA-XXXX-XXXX-XXXX when present`); + } else if (ghsa && advisoryId !== ghsa) { + violations.push( + `${label}: metadata.advisory-id must match metadata.ghsa when both are present`, + ); + } + + const advisoryUrl = String(metadata["advisory-url"] ?? ""); + const expectedGhsaUrl = GHSA_RE.test(advisoryId) + ? `https://github.com/openclaw/openclaw/security/advisories/${advisoryId}` + : ""; + if (!hasNonEmptyString(advisoryUrl)) { + violations.push(`${label}: missing metadata.advisory-url`); + } else if (expectedGhsaUrl && advisoryUrl !== expectedGhsaUrl) { + violations.push(`${label}: metadata.advisory-url must be ${expectedGhsaUrl}`); + } + + if (metadata["detector-bucket"] !== "precise") { + violations.push(`${label}: metadata.detector-bucket must be precise`); + } + if (!hasNonEmptyString(metadata["source-rule-id"])) { + violations.push(`${label}: missing metadata.source-rule-id`); + } + } + + return violations; +} + +export async function checkRulepack(rulepackPath = DEFAULT_RULEPACK) { + const rules = await readRules(rulepackPath); + return validateRuleMetadata(rules); +} + +export async function main(argv = process.argv.slice(2)) { + if (argv.includes("--help") || argv.includes("-h")) { + printHelp(); + return 0; + } + const rulepackPath = path.resolve(argv[0] ?? DEFAULT_RULEPACK); + const violations = await checkRulepack(rulepackPath); + if (violations.length > 0) { + console.error( + `check-opengrep-rule-metadata: ${violations.length} violation(s) in ${rulepackPath}`, + ); + for (const violation of violations.slice(0, 50)) { + console.error(` - ${violation}`); + } + if (violations.length > 50) { + console.error(` ... ${violations.length - 50} more`); + } + return 1; + } + console.log(`check-opengrep-rule-metadata: ${rulepackPath} ok`); + return 0; +} + +if (import.meta.main) { + process.exitCode = await main(); +} diff --git a/security/opengrep/compile-rules.mjs b/security/opengrep/compile-rules.mjs new file mode 100644 index 00000000000..3b062052e8d --- /dev/null +++ b/security/opengrep/compile-rules.mjs @@ -0,0 +1,602 @@ +#!/usr/bin/env node +/** + * compile-rules.mjs + * + * Compiles source OpenGrep rule YAML files from a folder into OpenClaw's shipped + * precise super-config. The input folder is intentionally generic: any nested + * .yml/.yaml file containing a top-level `rules` array can be compiled as long + * as each rule carries metadata.ghsa or metadata.advisory-id. + */ + +import { spawn } from "node:child_process"; +import { promises as fs } from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseDocument, stringify } from "yaml"; + +const REPO_BASENAME = "openclaw/openclaw"; +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(SCRIPT_DIR, "..", ".."); +const DEFAULT_OUT_DIR = path.resolve(REPO_ROOT, "security", "opengrep"); +const GHSA_RE = /^GHSA-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}$/; + +function printHelp() { + console.log(`Usage: node security/opengrep/compile-rules.mjs --rules-dir [options] + +Options: + --rules-dir Required. Directory containing source OpenGrep YAML files. + --out-dir Output directory for precise.yml (default: /security/opengrep). + --advisory-repo GitHub owner/repo used in advisory-url metadata. + Default: ${REPO_BASENAME} + --replace-precise Replace precise.yml instead of appending new rule ids. + --help Show this help. +`); +} + +function parseArgs(argv) { + const opts = { + rulesDir: "", + outDir: "", + advisoryRepo: REPO_BASENAME, + replacePrecise: false, + }; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + switch (arg) { + case "--rules-dir": + opts.rulesDir = path.resolve(argv[i + 1] ?? ""); + i += 1; + break; + case "--run-dir": + throw new Error( + "--run-dir was replaced by --rules-dir; pass a folder of source rule YAML files", + ); + case "--out-dir": + opts.outDir = path.resolve(argv[i + 1] ?? ""); + i += 1; + break; + case "--advisory-repo": + opts.advisoryRepo = argv[i + 1] ?? REPO_BASENAME; + i += 1; + break; + case "--replace-precise": + opts.replacePrecise = true; + break; + case "--help": + case "-h": + printHelp(); + process.exit(0); + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + if (!opts.rulesDir) { + printHelp(); + throw new Error("--rules-dir is required"); + } + return opts; +} + +function sanitizeIdComponent(value) { + return ( + String(value || "") + .replace(/[^a-zA-Z0-9._-]+/g, "-") + .replace(/^-+|-+$/g, "") + .toLowerCase() || "rule" + ); +} + +function normalizeSourceId(value) { + return String(value || "") + .trim() + .toUpperCase(); +} + +function sanitizeSourceIdComponent(value) { + return sanitizeIdComponent(value).replace(/[.]+/g, "-"); +} + +function sourceIdFromMetadata(metadata) { + return normalizeSourceId(metadata?.["advisory-id"] || metadata?.ghsa); +} + +function buildGhsaAdvisoryUrl(advisoryRepo, ghsa) { + return `https://github.com/${advisoryRepo}/security/advisories/${ghsa}`; +} + +function toPortablePath(filePath, repoRoot = REPO_ROOT) { + const resolved = path.resolve(filePath); + const relative = path.relative(repoRoot, resolved); + if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) { + return relative.split(path.sep).join("/"); + } + return path.basename(resolved); +} + +function rewriteRule(rule, params) { + const originalId = String(rule.id ?? "rule"); + const metadata = { ...(rule.metadata ?? {}) }; + const sourceId = sourceIdFromMetadata(metadata); + if (!sourceId) { + throw new Error( + `${params.sourceFile}: rule ${originalId} must set metadata.advisory-id or metadata.ghsa`, + ); + } + + if (GHSA_RE.test(sourceId)) { + metadata.ghsa = sourceId; + metadata["advisory-url"] = + metadata["advisory-url"] || buildGhsaAdvisoryUrl(params.advisoryRepo, sourceId); + } else if (!metadata["advisory-url"]) { + throw new Error( + `${params.sourceFile}: rule ${originalId} must set metadata.advisory-url for non-GHSA source ${sourceId}`, + ); + } + + metadata["advisory-id"] = sourceId; + metadata["detector-bucket"] = "precise"; + metadata["source-rule-id"] = originalId; + metadata["source-file"] = toPortablePath(params.sourceFile); + const newId = `${sanitizeSourceIdComponent(sourceId)}.${sanitizeIdComponent(originalId)}`; + return { ...rule, id: newId, metadata }; +} + +async function readRuleFile(filePath) { + const raw = await fs.readFile(filePath, "utf8"); + if (!raw.trim()) { + return { rules: [], error: null }; + } + let doc; + try { + doc = parseDocument(raw, { keepSourceTokens: false }); + } catch (error) { + return { rules: [], error: `parse-error: ${error.message}` }; + } + if (doc.errors && doc.errors.length > 0) { + return { rules: [], error: `yaml-errors: ${doc.errors.map((e) => e.message).join("; ")}` }; + } + const data = doc.toJSON(); + if (!data || !Array.isArray(data.rules)) { + return { rules: [], error: "no-rules-array" }; + } + return { rules: data.rules, error: null }; +} + +async function listYamlFiles(dir) { + const out = []; + async function walk(current) { + const entries = await fs.readdir(current, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === ".git") { + continue; + } + await walk(fullPath); + } else if (entry.isFile() && /\.ya?ml$/i.test(entry.name)) { + if (entry.name === "precise.yml") { + continue; + } + out.push(fullPath); + } + } + } + await walk(dir); + return out.toSorted(); +} + +async function compile(opts) { + const sourceFiles = await listYamlFiles(opts.rulesDir); + const buckets = { + precise: { rules: [], skipped: [] }, + }; + const manifest = { + rulesDir: toPortablePath(opts.rulesDir), + advisoryRepo: opts.advisoryRepo, + generatedAt: new Date().toISOString(), + totals: {}, + files: {}, + }; + + for (const filePath of sourceFiles) { + const fileKey = toPortablePath(filePath); + const fileEntry = { precise: [], errors: {} }; + const { rules, error } = await readRuleFile(filePath); + if (error) { + buckets.precise.skipped.push({ file: fileKey, error }); + fileEntry.errors.precise = error; + } else { + for (const rule of rules) { + try { + const rewritten = rewriteRule(rule, { + advisoryRepo: opts.advisoryRepo, + sourceFile: filePath, + }); + buckets.precise.rules.push(rewritten); + fileEntry.precise.push(rewritten.id); + } catch (error_) { + const errorMessage = error_ instanceof Error ? error_.message : String(error_); + buckets.precise.skipped.push({ file: fileKey, error: errorMessage }); + fileEntry.errors.precise = errorMessage; + } + } + } + if (fileEntry.precise.length || Object.keys(fileEntry.errors).length) { + manifest.files[fileKey] = fileEntry; + } + } + + manifest.totals = { + filesScanned: sourceFiles.length, + filesWithAnyRule: Object.keys(manifest.files).length, + preciseRulesGenerated: buckets.precise.rules.length, + preciseSkipped: buckets.precise.skipped.length, + }; + + return { buckets, manifest }; +} + +function buildBucketHeader(bucket, manifest, ruleCount) { + const count = ruleCount ?? manifest.totals.preciseRules; + return [ + `# OpenGrep super-config: ${bucket}`, + `#`, + `# Auto-generated by security/opengrep/compile-rules.mjs.`, + `# DO NOT EDIT BY HAND. Re-run the compile script after editing source rules.`, + `#`, + `# Source rules dir: ${manifest.rulesDir}`, + `# Generated at : ${manifest.generatedAt}`, + `# Rule count : ${count}`, + "", + ].join("\n"); +} + +async function readExistingRules(filePath) { + const { rules, error } = await readRuleFile(filePath); + if (error) { + throw new Error(`Could not read existing precise rules from ${filePath}: ${error}`); + } + return rules; +} + +function appendNewRules(existingRules, generatedRules) { + const existingIds = new Set(existingRules.map((rule) => String(rule.id ?? ""))); + const appendedRules = []; + const skippedDuplicateIds = []; + for (const rule of generatedRules) { + const id = String(rule.id ?? ""); + if (existingIds.has(id)) { + skippedDuplicateIds.push(id); + continue; + } + existingIds.add(id); + appendedRules.push(rule); + } + return { + rules: [...existingRules, ...appendedRules], + appendedRules, + skippedDuplicateIds, + }; +} + +function detectIdCollisions(rules) { + const seen = new Map(); + const dupes = []; + for (const r of rules) { + if (seen.has(r.id)) { + dupes.push({ id: r.id, ghsas: [seen.get(r.id), r.metadata?.ghsa] }); + } else { + seen.set(r.id, r.metadata?.ghsa || ""); + } + } + return dupes; +} + +function disambiguateCollisions(rules) { + const seen = new Map(); + const out = []; + for (const r of rules) { + let id = r.id; + if (seen.has(id)) { + const next = (seen.get(id) ?? 1) + 1; + seen.set(id, next); + id = `${id}-${next}`; + } else { + seen.set(id, 1); + } + out.push({ ...r, id }); + } + return out; +} + +function runCommand(argv, options = {}) { + return new Promise((resolve) => { + const { timeoutMs, ...spawnOptions } = options; + const child = spawn(argv[0], argv.slice(1), { + stdio: ["ignore", "pipe", "pipe"], + ...spawnOptions, + }); + let stdout = ""; + let stderr = ""; + let settled = false; + const finish = (result) => { + if (settled) { + return; + } + settled = true; + if (timer) { + clearTimeout(timer); + } + resolve(result); + }; + const timer = + timeoutMs && timeoutMs > 0 + ? setTimeout(() => { + child.kill("SIGKILL"); + finish({ code: null, stdout, stderr, timedOut: true }); + }, timeoutMs) + : null; + child.stdout.on("data", (chunk) => (stdout += chunk)); + child.stderr.on("data", (chunk) => (stderr += chunk)); + child.on("close", (code) => finish({ code, stdout, stderr, timedOut: false })); + child.on("error", (err) => finish({ code: -1, stdout, stderr: String(err), timedOut: false })); + }); +} + +async function findInvalidRuleSpans(superConfigPath) { + const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), "opengrep-empty-")); + try { + const result = await runCommand( + [ + "opengrep", + "scan", + "--no-strict", + "--config", + superConfigPath, + "--json", + "--no-git-ignore", + emptyDir, + ], + { timeoutMs: 120_000 }, + ); + if (!result.stdout || result.stdout.trim() === "") { + const tail = (result.stderr || "").trim().slice(-500); + return { + invalidLines: new Set(), + errorCount: 0, + validatorOk: false, + validatorError: `opengrep produced no JSON output (exit code ${result.code}). stderr tail: ${tail || "(empty)"}`, + }; + } + let parsed; + try { + parsed = JSON.parse(result.stdout); + } catch (parseErr) { + return { + invalidLines: new Set(), + errorCount: 0, + validatorOk: false, + validatorError: `opengrep stdout was not valid JSON (exit code ${result.code}): ${String(parseErr).slice(0, 200)}`, + }; + } + const invalidLines = new Set(); + const invalidRuleIds = new Set(); + const unmappedErrors = []; + let errorCount = 0; + for (const err of parsed.errors || []) { + const ruleId = typeof err.rule_id === "string" ? err.rule_id : ""; + if (ruleId) { + invalidRuleIds.add(ruleId); + errorCount += 1; + continue; + } + if (err.type === "InvalidRuleSchemaError") { + errorCount += 1; + for (const span of err.spans || []) { + const start = span.start?.line; + const end = span.end?.line ?? start; + if (typeof start === "number" && typeof end === "number") { + for (let line = start; line <= end; line += 1) { + invalidLines.add(line); + } + } + } + if (!err.spans || err.spans.length === 0) { + unmappedErrors.push(err.type); + } + continue; + } + unmappedErrors.push(err.type || "unknown"); + } + if (result.code !== 0 && unmappedErrors.length > 0) { + return { + invalidLines, + invalidRuleIds, + errorCount, + validatorOk: false, + validatorError: `opengrep exited ${result.code} with unmapped errors: ${unmappedErrors.join(", ")}`, + }; + } + if (result.code !== 0 && invalidLines.size === 0 && invalidRuleIds.size === 0) { + return { + invalidLines, + invalidRuleIds, + errorCount, + validatorOk: false, + validatorError: `opengrep exited ${result.code} with no mappable rule errors`, + }; + } + return { invalidLines, invalidRuleIds, errorCount, validatorOk: true }; + } finally { + await fs.rm(emptyDir, { recursive: true, force: true }).catch(() => {}); + } +} + +function rulesOverlappingLines(superConfigText, invalidLines) { + const lines = superConfigText.split("\n"); + const ruleStarts = []; + for (let i = 0; i < lines.length; i += 1) { + if (/^\s{2}-\s+id:\s*/.test(lines[i])) { + ruleStarts.push(i + 1); + } + } + const bad = new Set(); + for (const ln of invalidLines) { + let lo = 0; + let hi = ruleStarts.length - 1; + let pick = -1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (ruleStarts[mid] <= ln) { + pick = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + if (pick >= 0) { + bad.add(pick); + } + } + return bad; +} + +async function pruneInvalidRulesForBucket(rules, manifest, bucket, outDir, maxIterations = 4) { + let working = rules.slice(); + const droppedDetails = []; + for (let iter = 0; iter < maxIterations; iter += 1) { + const yamlText = + buildBucketHeader(bucket, manifest, working.length) + + stringify({ rules: working }, { lineWidth: 0 }); + const tmpPath = path.join(outDir, `.tmp-${bucket}.yml`); + await fs.writeFile(tmpPath, yamlText); + const { invalidLines, invalidRuleIds, errorCount, validatorOk, validatorError } = + await findInvalidRuleSpans(tmpPath); + await fs.rm(tmpPath, { force: true }).catch(() => {}); + if (!validatorOk) { + throw new Error( + `opengrep schema validation failed for bucket '${bucket}'. Install opengrep ` + + `(https://opengrep.dev) and retry. Validator error: ${validatorError}`, + ); + } + if ( + errorCount === 0 || + (invalidLines.size === 0 && (!invalidRuleIds || invalidRuleIds.size === 0)) + ) { + return { rules: working, droppedDetails }; + } + const badIndices = rulesOverlappingLines(yamlText, invalidLines); + if (invalidRuleIds && invalidRuleIds.size > 0) { + for (let i = 0; i < working.length; i += 1) { + const ruleId = String(working[i].id ?? ""); + for (const invalidRuleId of invalidRuleIds) { + if (invalidRuleId === ruleId || invalidRuleId.endsWith(`.${ruleId}`)) { + badIndices.add(i); + break; + } + } + } + } + if (badIndices.size === 0) { + throw new Error( + `opengrep reported ${errorCount} invalid ${bucket} rule(s), but the compiler could not map them to generated rules`, + ); + } + const next = []; + for (let i = 0; i < working.length; i += 1) { + if (badIndices.has(i)) { + droppedDetails.push({ + id: working[i].id, + ghsa: working[i].metadata?.ghsa, + }); + } else { + next.push(working[i]); + } + } + working = next; + } + return { rules: working, droppedDetails }; +} + +async function writeOutputs(buckets, manifest, outDir, opts) { + await fs.mkdir(outDir, { recursive: true }); + + const precisePath = path.join(outDir, "precise.yml"); + const existingRules = opts.replacePrecise ? [] : await readExistingRules(precisePath); + const collisions = detectIdCollisions(buckets.precise.rules); + if (collisions.length > 0) { + console.error( + `[warn] precise: ${collisions.length} duplicate generated rule ids will be auto-suffixed (-2, -3, ...).`, + ); + } + const disambiguated = disambiguateCollisions(buckets.precise.rules); + const appendResult = opts.replacePrecise + ? { rules: disambiguated, appendedRules: disambiguated, skippedDuplicateIds: [] } + : appendNewRules(existingRules, disambiguated); + + let validRules = appendResult.rules; + let droppedDetails = []; + if (appendResult.rules.length > 0) { + console.error(`[info] precise: validating ${appendResult.rules.length} rules with opengrep...`); + ({ rules: validRules, droppedDetails } = await pruneInvalidRulesForBucket( + appendResult.rules, + manifest, + "precise", + outDir, + )); + } else { + console.error("[info] precise: no rules to validate with opengrep."); + } + buckets.precise.invalid = droppedDetails; + if (droppedDetails.length > 0) { + console.error(`[warn] precise: dropped ${droppedDetails.length} rules with invalid schema.`); + } + + const yaml = stringify({ rules: validRules }, { lineWidth: 0 }); + await fs.writeFile(precisePath, buildBucketHeader("precise", manifest, validRules.length) + yaml); + + manifest.totals.preciseRulesExisting = existingRules.length; + manifest.totals.preciseRulesAppended = appendResult.appendedRules.length; + manifest.totals.preciseRulesDuplicateSkipped = appendResult.skippedDuplicateIds.length; + manifest.totals.preciseRules = validRules.length; + manifest.totals.preciseInvalid = droppedDetails.length; + manifest.preciseInvalid = droppedDetails; + manifest.preciseDuplicateSkipped = appendResult.skippedDuplicateIds; +} + +function printSummary(buckets, manifest, outDir) { + console.log(`compile-rules: done`); + console.log(` out-dir : ${outDir}`); + console.log(` files scanned : ${manifest.totals.filesScanned}`); + console.log(` files with rules : ${manifest.totals.filesWithAnyRule}`); + console.log( + ` precise rules : ${manifest.totals.preciseRules} total (${manifest.totals.preciseRulesExisting ?? 0} existing, ${manifest.totals.preciseRulesAppended ?? 0} appended, ${manifest.totals.preciseRulesDuplicateSkipped ?? 0} duplicate skipped, yaml-skipped: ${manifest.totals.preciseSkipped}, schema-invalid: ${manifest.totals.preciseInvalid ?? 0})`, + ); + const totalDropped = + (manifest.totals.preciseSkipped ?? 0) + (manifest.totals.preciseInvalid ?? 0); + if (totalDropped > 0) { + console.log("\nFirst few skipped/invalid rules:"); + for (const s of (buckets.precise.skipped ?? []).slice(0, 3)) { + console.log(` [precise] ${s.file}: yaml: ${s.error.split("\n")[0]}`); + } + for (const s of (buckets.precise.invalid ?? []).slice(0, 3)) { + console.log(` [precise] ${s.id}: schema-invalid`); + } + } +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + if (!opts.outDir) { + opts.outDir = DEFAULT_OUT_DIR; + } + const { buckets, manifest } = await compile(opts); + await writeOutputs(buckets, manifest, opts.outDir, opts); + printSummary(buckets, manifest, opts.outDir); +} + +main().catch((err) => { + console.error(`compile-rules: error: ${err.message ?? err}`); + process.exit(1); +}); diff --git a/security/opengrep/precise.yml b/security/opengrep/precise.yml new file mode 100644 index 00000000000..836cbe0a9ae --- /dev/null +++ b/security/opengrep/precise.yml @@ -0,0 +1,4978 @@ +# OpenGrep super-config: precise +# +# Auto-generated by security/opengrep/compile-rules.mjs. +# DO NOT EDIT BY HAND. Re-run the compile script after editing source rules. +# +# Source rules dir: +# Generated at : 2026-04-29T07:10:35.427Z +# Rule count : 147 +rules: + - id: ghsa-25gx-x37c-7pph.openclaw-novnc-x11vnc-missing-auth + message: x11vnc starts without VNC authentication; avoid -nopw and require password auth when exposing noVNC observer access. + severity: ERROR + languages: + - regex + pattern-regex: x11vnc[^\n]*-nopw + metadata: + ghsa: GHSA-25GX-X37C-7PPH + cwe: CWE-306 + category: security + technology: + - x11vnc + - novnc + - vnc + confidence: high + likelihood: medium + impact: medium + references: + - https://github.com/openclaw/openclaw/security/advisories/GHSA-25gx-x37c-7pph + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-25GX-X37C-7PPH + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-novnc-x11vnc-missing-auth + - id: ghsa-25pw-4h6w-qwvm.openclaw.imessage-group-allowlist-merged-with-pairing-store + languages: + - typescript + severity: ERROR + message: DM pairing-store identities are merged into a group allowlist. Group authorization must stay explicit and should not inherit DM pairing-store entries. + patterns: + - pattern: | + const $STORE = await readChannelAllowFromStore(...).catch(() => []); + ... + const $GROUP = Array.from(new Set([...$GROUPALLOW, ...$STORE])) + .map((v) => String(v).trim()) + .filter(Boolean); + metadata: + category: security + cwe: CWE-863 + ghsa: GHSA-25PW-4H6W-QWVM + confidence: high + note: Captures the original iMessage vulnerable merge shape exactly and generalizes to same-family explicit merge sites. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-25PW-4H6W-QWVM + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.imessage-group-allowlist-merged-with-pairing-store + - id: ghsa-2prf-9cw7-fq62.whatsapp-reaction-requires-allowfrom-guard + message: WhatsApp reaction actions should validate chatJid with resolveWhatsAppOutboundTarget before sendReactionWhatsApp. + severity: ERROR + languages: + - typescript + patterns: + - pattern-inside: | + if ($ACTION === "react") { + ... + await sendReactionWhatsApp($TARGET, $MSG, $EMOJI, ...) + ... + } + - pattern: | + await sendReactionWhatsApp($TARGET, $MSG, $EMOJI, ...) + - metavariable-pattern: + metavariable: $TARGET + language: generic + patterns: + - pattern-not: $RES.to + - pattern-not-inside: | + $RES = resolveWhatsAppOutboundTarget({ + ..., + to: $TARGET, + ... + }); + ... + if (!$RES.ok) { + ... + } + ... + await sendReactionWhatsApp($RES.to, $MSG, $EMOJI, ...) + metadata: + category: security + cwe: + - CWE-862 + - CWE-20 + ghsa: GHSA-2PRF-9CW7-FQ62 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-2PRF-9CW7-FQ62 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: whatsapp-reaction-requires-allowfrom-guard + - id: ghsa-2qj5-gwg2-xwc4.openclaw.prompt-unsanitized-literal-interpolation + languages: + - typescript + - javascript + severity: ERROR + message: | + Untrusted runtime path/URL value flows into a string literal inside a prompt-builder function, without going through sanitizeForPromptLiteral or wrapUntrustedPromptDataBlock. Workspace paths can contain Unicode control / format characters that break LLM prompt structure (CVE-2026-27001 / GHSA-2qj5-gwg2-xwc4). + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern: $X.workspaceDir + - pattern: $X.containerWorkspaceDir + - pattern: $X.agentWorkspaceMount + - pattern: $X.browserNoVncUrl + - pattern: $X.displayWorkspaceDir + - pattern: $X.mountPath + - pattern: $X.canvasRootDir + - pattern: $X.cwd + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: sanitizeForPromptLiteral(...) + - pattern: sanitizeForPromptLiterals(...) + - pattern: wrapUntrustedPromptDataBlock(...) + - patterns: + - pattern: $F(...) + - metavariable-regex: + metavariable: $F + regex: ^(sanitize|escape|validate)[A-Z]\w*Prompt\w*$ + pattern-sinks: + - patterns: + - pattern-either: + - pattern: | + `...${$VALUE}...` + - pattern: | + "..." + $VALUE + "..." + - pattern: | + '...' + $VALUE + '...' + - pattern-either: + - pattern-inside: | + function $FN(...) { ... } + - pattern-inside: | + const $FN = (...) => { ... } + - pattern-inside: | + export function $FN(...) { ... } + - pattern-inside: | + export const $FN = (...) => { ... } + - metavariable-regex: + metavariable: $FN + regex: ^(build|render|compose|construct|format)[A-Z]\w*Prompt\w*$ + metadata: + ghsa: GHSA-2QJ5-GWG2-XWC4 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-2QJ5-GWG2-XWC4 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.prompt-unsanitized-literal-interpolation + - id: ghsa-2qrv-rc5x-2g2h.openclaw-untrusted-workspace-channel-shadow-setup + languages: + - typescript + - javascript + severity: ERROR + mode: taint + message: Workspace-aware channel-plugin catalog lookup at channel-setup time reaches a use site without going through resolveTrustedCatalogEntry() or excludeWorkspace:true. Workspace-defined channel shadows could execute during built-in channel setup. See GHSA-2QRV-RC5X-2G2H. + metadata: + category: security + ghsas: + - GHSA-2QRV-RC5X-2G2H + ghsa: GHSA-2QRV-RC5X-2G2H + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-2QRV-RC5X-2G2H + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-untrusted-workspace-channel-shadow-setup + paths: + include: + - src/commands/channel-setup/*.ts + - src/flows/channel-setup.ts + - src/commands/channels/add.ts + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - src/commands/channel-setup/trusted-catalog.ts + pattern-sources: + - patterns: + - pattern-either: + - pattern: getChannelPluginCatalogEntry(...) + - pattern: listChannelPluginCatalogEntries(...) + - pattern-not-regex: \bexcludeWorkspace\s*:\s*true\b + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: resolveTrustedCatalogEntry(...) + - pattern: listTrustedChannelPluginCatalogEntries(...) + - pattern: listSetupDiscoveryChannelPluginCatalogEntries(...) + pattern-sinks: + - patterns: + - pattern-either: + - pattern: $X.filter(...) + - pattern: $X.map(...) + - pattern: $X.flatMap(...) + - pattern: $X.find(...) + - pattern: $X.forEach(...) + - pattern: $X.some(...) + - pattern: $X.every(...) + - pattern: $X.id + - pattern: $X.meta + - pattern: $X.plugin + - pattern: $X[$I] + - pattern: return $X; + - pattern: new Map($X) + - pattern: new Set($X) + - id: ghsa-2rgf-hm63-5qph.xff-last-hop-without-trusted-proxy-context + languages: + - typescript + - javascript + severity: WARNING + message: Review X-Forwarded-For parsers that pick the last header hop directly instead of walking past trusted proxies. + metadata: + category: security + cwe: + - CWE-345 + - CWE-807 + ghsa: GHSA-2RGF-HM63-5QPH + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-2RGF-HM63-5QPH + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: xff-last-hop-without-trusted-proxy-context + pattern-either: + - patterns: + - pattern: | + const $ENTRIES = $FORWARDED + ?.split(",") + .map(($ENTRY) => $MAP) + .filter(($FILTER) => $PRED); + const $RAW = $ENTRIES?.at(-1); + ... + return $NORMALIZE($STRIP($RAW)); + - patterns: + - pattern: | + const $HOPS = $FORWARDED + ?.split(",") + .map(($ENTRY) => $MAP) + .filter(($FILTER) => $PRED); + ... + return $SANITIZE($HOPS[$HOPS.length - 1]); + - id: ghsa-2x4x-cc5g-qmmg.node-pair-approve-missing-callerscopes-object + languages: + - typescript + - javascript + severity: ERROR + message: approveNodePairing on node approval paths should receive an options object that carries callerScopes. + patterns: + - pattern-either: + - pattern: approveNodePairing($REQ) + - pattern: approveNodePairing($REQ, undefined, ...) + metadata: + ghsa: GHSA-2X4X-CC5G-QMMG + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-2X4X-CC5G-QMMG + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: node-pair-approve-missing-callerscopes-object + - id: ghsa-3298-56p6-rpw2.openclaw-killprocesstree-from-shell-utils + languages: + - typescript + severity: ERROR + message: killProcessTree from agents/shell-utils performs an immediate ungraceful SIGKILL on the entire process tree. Use the killProcessTree from src/process/kill-tree.ts (which sends SIGTERM, waits graceMs, then SIGKILL only if needed). See GHSA-3298-56P6-RPW2. + metadata: + category: security + cwe: + - CWE-697 + ghsas: + - GHSA-3298-56P6-RPW2 + ghsa: GHSA-3298-56P6-RPW2 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-3298-56P6-RPW2 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-killprocesstree-from-shell-utils + paths: + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - src/process/kill-tree.ts + pattern-either: + - pattern: import { ..., killProcessTree, ... } from "../shell-utils" + - pattern: import { ..., killProcessTree, ... } from "./shell-utils" + - pattern: import { ..., killProcessTree, ... } from "../agents/shell-utils" + - pattern: import { ..., killProcessTree, ... } from "src/agents/shell-utils" + - pattern: import { ..., killProcessTree, ... } from "../../agents/shell-utils" + - pattern: | + export function killProcessTree($PID: number): void { ... process.kill($X, "SIGKILL"); ... } + - id: ghsa-33hm-cq8r-wc49.openclaw-sandbox-tmp-accepts-os-tmpdir + languages: + - typescript + - javascript + message: Sandbox tmp-path validation trusts any os.tmpdir() path instead of constraining paths to the active sandbox root or OpenClaw-managed temp roots. + severity: ERROR + patterns: + - pattern-either: + - pattern: | + const $TMP = path.resolve(os.tmpdir()) + ... + if (!isPathInside($TMP, $RESOLVED)) { + return undefined; + } + ... + return $RESOLVED; + - pattern: | + const $TMP = path.resolve(os.tmpdir()) + ... + if (!isPathInside($TMP, $ABS)) { + return undefined; + } + ... + return $ABS; + metadata: + category: security + cwe: + - CWE-22 + - CWE-284 + ghsa: GHSA-33HM-CQ8R-WC49 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-33HM-CQ8R-WC49 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-sandbox-tmp-accepts-os-tmpdir + - id: ghsa-354r-7mfh-7rh2.review-reaction-enqueue-without-dm-access-check + languages: + - typescript + - javascript + severity: WARNING + message: Reaction handlers that enqueue system events without calling resolveDmGroupAccessWithLists need review for DM authorization parity. + paths: + include: + - src/discord/monitor/listeners.ts + - src/slack/monitor/events/reactions.ts + patterns: + - pattern-either: + - pattern: | + async function $F(...){ + ... + enqueueSystemEvent($TEXT, $OPTS) + ... + } + - pattern: | + const $F = async (...) => { + ... + enqueueSystemEvent($TEXT, $OPTS) + ... + } + - pattern-regex: reaction|Reaction|messageReaction|reaction_added|reaction_removed + - pattern-not-regex: resolveDmGroupAccessWithLists + metadata: + ghsa: GHSA-354R-7MFH-7RH2 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-354R-7MFH-7RH2 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: review-reaction-enqueue-without-dm-access-check + - id: ghsa-36h3-7c54-j27r.browser-route-tempdir-write-missing-symlink-guard + message: Browser route handler writes to a path under the managed temp root without going through resolveWritablePathWithinRoot. The gate is needed to refuse symlink-root and symlink-parent escapes from the temp dir (otherwise an attacker can place a symlink at any directory component and escape the boundary). See GHSA-36H3-7C54-J27R. + severity: ERROR + languages: + - typescript + metadata: + ghsa: GHSA-36H3-7C54-J27R + category: security + cwe: + - CWE-22 + - CWE-59 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-36H3-7C54-J27R + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: browser-route-tempdir-write-missing-symlink-guard + paths: + include: + - extensions/browser/src/browser/routes/output-paths.ts + - extensions/browser/src/browser/routes/agent.act*.ts + - extensions/browser/src/browser/routes/agent.debug*.ts + patterns: + - pattern-either: + - pattern: fs.mkdir(...) + - pattern: fs.mkdirSync(...) + - pattern: fsp.mkdir(...) + - pattern: fs.createWriteStream(...) + - pattern: fs.writeFile(...) + - pattern: fsp.writeFile(...) + - pattern-not-inside: | + import { ..., resolveWritablePathWithinRoot, ... } from "$X"; + ... + - pattern-not-inside: | + import { resolveWritablePathWithinRoot } from "$X"; + ... + - pattern-not-inside: | + import { resolveWritablePathWithinRoot, ... } from "$X"; + ... + - id: ghsa-39mp-545q-w789.openclaw-owner-only-send-policy-mutation + message: Owner-only /send handlers must reject non-owner senders before mutating sessionEntry.sendPolicy. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-inside: | + const $PARSED = parseSendPolicyCommand($CMD); + ... + - pattern: | + if (!$PARAMS.command.isAuthorizedSender) { + ... + return ...; + } + ... + if ($PARAMS.sessionEntry && $PARAMS.sessionStore && $PARAMS.sessionKey) { + ... + $MUTATION + ... + } + - metavariable-pattern: + metavariable: $MUTATION + patterns: + - pattern-either: + - pattern: $PARAMS.sessionEntry.sendPolicy = $MODE; + - pattern: delete $PARAMS.sessionEntry.sendPolicy; + - pattern-not-inside: | + const $NONOWNER = rejectNonOwnerCommand($PARAMS, "/send"); + if ($NONOWNER) { + return $NONOWNER; + } + - pattern-not-inside: | + if (!$PARAMS.command.senderIsOwner) { + ... + return ...; + } + metadata: + ghsa: GHSA-39MP-545Q-W789 + category: authorization + detector: A + coverage_requirement: vulnerable_fix_diff_files + review_surface: owner_only_send_policy + rationale: catches /send policy mutations guarded only by general authorization + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-39MP-545Q-W789 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-owner-only-send-policy-mutation + - id: ghsa-3cw3-5vxw-g2h3.discovery-txt-hint-routing + languages: + - swift + severity: ERROR + message: Discovery helpers should not fall back to TXT-derived lanHost or tailnetDns values when building SSH targets or direct gateway URLs. Use resolved service endpoints only. + patterns: + - pattern-either: + - pattern: let host = self.sanitizedTailnetHost($GW.tailnetDns) ?? $GW.lanHost + - pattern: let host = sanitizeTailnet($GW.tailnetDns) ?? $GW.lanHost + - pattern: guard let lanHost = self.trimmed(lanHost), !lanHost.isEmpty else { return nil } + - pattern: let port = gatewayPort ?? 18789 + metadata: + ghsa: GHSA-3CW3-5VXW-G2H3 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-3CW3-5VXW-G2H3 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: discovery-txt-hint-routing + - id: ghsa-3hcm-ggvf-rch5.exec-allowlist-double-quoted-command-substitution + languages: + - typescript + - javascript + severity: ERROR + message: Shell allowlist parsers that stay in double-quote mode without rejecting command substitution or backticks can approve commands that execute hidden substitutions. + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + if ($IN_DOUBLE) { + ... + } + - pattern: | + if ($CH === "\"") { + $IN_DOUBLE = false; + } + ... + $BUF += $CH; + - pattern-not: | + if ($CH === "$" && $NEXT === "(") { + ... + } + - pattern-not: | + if ($CH === "`") { + ... + } + - patterns: + - pattern-inside: | + if ($IN_DOUBLE) { + ... + } + - pattern: | + if ($CH === "\"") { + $IN_DOUBLE = false; + } else { + $BUF += $CH; + } + - pattern-not: | + if ($CH === "$" && $NEXT === "(") { + ... + } + - pattern-not: | + if ($CH === "`") { + ... + } + metadata: + ghsa: GHSA-3HCM-GGVF-RCH5 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-3HCM-GGVF-RCH5 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: exec-allowlist-double-quoted-command-substitution + - id: ghsa-3q42-xmxv-9vfr.openclaw-talk-voice-set-without-admin-scope-check + message: Command handlers that persist talk voice config via writeConfigFile should reject gateway-scoped callers lacking operator.admin before writing the config. + severity: ERROR + languages: + - typescript + metadata: + ghsa: GHSA-3Q42-XMXV-9VFR + cwe: CWE-269 + detector: A + confidence: low + category: security + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-3Q42-XMXV-9VFR + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-talk-voice-set-without-admin-scope-check + patterns: + - pattern: | + if ($ACTION === "set") { + ... + await $RUNTIME.config.writeConfigFile($NEXT) + ... + } + - pattern-not: | + if ($ACTION === "set") { + ... + if (requiresAdminToSetVoice($CTX.channel, $CTX.gatewayClientScopes)) { + return ...; + } + ... + await $RUNTIME.config.writeConfigFile($NEXT) + ... + } + - pattern-not: | + if ($ACTION === "set") { + ... + if (! $CTX.gatewayClientScopes?.includes("operator.admin")) { + return ...; + } + ... + await $RUNTIME.config.writeConfigFile($NEXT) + ... + } + - id: ghsa-4685-c5cp-vp95.safe-bin-missing-blocked-option-policy + languages: + - typescript + - javascript + message: Safe-bin usage validators should reject dangerous sort/grep flags with hasBlockedSafeBinOption before accepting option tokens. + severity: ERROR + patterns: + - pattern-inside: | + function isSafeBinUsage(...) { + ... + } + - pattern: | + if ($TOKEN.startsWith("-")) { + ... + } + - pattern-not-inside: | + if ($TOKEN.startsWith("-")) { + if (hasBlockedSafeBinOption($EXEC, $TOKEN)) { + ... + } + ... + } + metadata: + ghsa: GHSA-4685-C5CP-VP95 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-4685-C5CP-VP95 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: safe-bin-missing-blocked-option-policy + - id: ghsa-49cg-279w-m73x.approval-auth-empty-approver-authorized + languages: + - typescript + severity: ERROR + message: Empty approver lists should stay implicit same-chat fallback; do not return explicit authorized approval auth when approvers.length === 0. + patterns: + - pattern-inside: | + if ($APPROVERS.length === 0) { + ... + } + - pattern-either: + - pattern: | + return { authorized: true } as const; + - pattern: | + return { authorized: true }; + metadata: + category: security + confidence: medium + technology: + - typescript + ghsa: GHSA-49CG-279W-M73X + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-49CG-279W-M73X + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: approval-auth-empty-approver-authorized + - id: ghsa-4cqv-h74h-93j4.discord-allowlist-name-matching-without-dangerous-opt-in + message: Discord allowlist matching uses mutable names/tags without an explicit dangerous opt-in. + languages: + - typescript + - javascript + severity: ERROR + patterns: + - pattern-either: + - pattern: | + allowListMatches($LIST, $CANDIDATE) + - pattern: | + resolveDiscordAllowListMatch({ + allowList: $LIST, + candidate: $CANDIDATE, + ..., + }) + - pattern: | + resolveDiscordAllowListMatch({ + ..., + allowList: $LIST, + candidate: $CANDIDATE, + }) + - pattern-not: | + allowListMatches($LIST, $CANDIDATE, { allowNameMatching: ... }) + - pattern-not: | + resolveDiscordAllowListMatch({ + allowList: $LIST, + candidate: $CANDIDATE, + allowNameMatching: ..., + ..., + }) + - pattern-not: | + resolveDiscordAllowListMatch({ + ..., + allowList: $LIST, + candidate: $CANDIDATE, + allowNameMatching: ..., + }) + metadata: + family: discord-allowlist-name-matching + review: reusable structural rule for missing dangerous opt-in + ghsa: GHSA-4CQV-H74H-93J4 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-4CQV-H74H-93J4 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: discord-allowlist-name-matching-without-dangerous-opt-in + - id: ghsa-4g5x-2jfc-xm98.raw-fetch-to-disk-bypasses-media-runtime + message: Remote media downloads should use the shared bounded media-runtime helpers instead of fetchWithSsrFGuard plus direct response-body-to-disk writes, which bypass core size, cleanup, and inbound-store limits. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-inside: | + async function $F(...){ + ... + } + - pattern: | + const { response, release } = await fetchWithSsrFGuard({ ... }); + ... + await pipeline(Readable.fromWeb($BODY), $WRITESTREAM); + ... + - pattern-not-inside: | + const $FETCHED = await fetchRemoteMedia({ ... }); + ... + - pattern-not-inside: | + await saveMediaBuffer(...) + metadata: + category: security + confidence: medium + ghsa: GHSA-4G5X-2JFC-XM98 + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-4G5X-2JFC-XM98 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: raw-fetch-to-disk-bypasses-media-runtime + - id: ghsa-4rj2-gpmh-qq5x.inbound-allowlist-bidirectional-endswith + languages: + - typescript + - javascript + severity: ERROR + message: Bidirectional endsWith on normalized caller and allowlist values can bypass inbound allowlists. Reject empty caller IDs and compare canonicalized identifiers with strict equality. + patterns: + - pattern-either: + - pattern: | + $FROM.endsWith($ALLOW) || $ALLOW.endsWith($FROM) + - pattern: | + $ALLOW.endsWith($FROM) || $FROM.endsWith($ALLOW) + metavariable-comparison: + metavariable: $FROM + comparison: str($FROM) != str($ALLOW) + metadata: + category: security + confidence: high + cwe: CWE-287 + ghsa: GHSA-4RJ2-GPMH-QQ5X + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-4RJ2-GPMH-QQ5X + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: inbound-allowlist-bidirectional-endswith + - id: ghsa-527m-976r-jf79.browser-existing-session-interaction-missing-navigation-guard + message: Existing-session browser interaction routes must call assertExistingSessionPostInteractionNavigationAllowed (or use withBrowserNavigationPolicy) to apply the SSRF navigation guard. Without this gate, click/keyboard/script interactions can navigate to internal IPs / cloud metadata endpoints that the operator hasn't allowed. See GHSA-527M-976R-JF79. + severity: ERROR + languages: + - typescript + metadata: + ghsa: GHSA-527M-976R-JF79 + category: security + cwe: + - CWE-918 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-527M-976R-JF79 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: browser-existing-session-interaction-missing-navigation-guard + paths: + include: + - extensions/browser/src/browser/routes/agent.act*.ts + exclude: + - extensions/browser/src/browser/routes/agent.act.hooks.ts + patterns: + - pattern-either: + - pattern: evaluateChromeMcpScript(...) + - pattern: clickChromeMcpElement(...) + - pattern: typeChromeMcpKeys(...) + - pattern-not-inside: | + import { ..., assertExistingSessionPostInteractionNavigationAllowed, ... } from "$X"; + ... + - pattern-not-inside: | + import { ..., assertBrowserNavigationResultAllowed, ... } from "$X"; + ... + - pattern-not-inside: | + import { assertBrowserNavigationResultAllowed } from "$X"; + ... + - id: ghsa-536q-mj95-h29h.openclaw-browser-interaction-navigation-guard + languages: + - typescript + severity: ERROR + message: Browser interaction helpers that expose opts.ssrfPolicy and perform press-based actions should route the action through assertInteractionNavigationCompletedSafely so delayed post-action navigations still receive SSRF enforcement. + patterns: + - pattern-either: + - pattern: | + export async function $FN(opts: $OPTS) { + ... + await $PAGE.keyboard.press(...); + ... + } + - pattern: | + export async function $FN(opts: $OPTS) { + ... + await $LOCATOR.press(...); + ... + } + - pattern: opts.ssrfPolicy + - pattern-not-inside: | + export async function $FN(opts: $OPTS) { + ... + await assertInteractionNavigationCompletedSafely({ + ... + }); + ... + } + - pattern-not-inside: | + export async function $FN(opts: $OPTS) { + ... + return await assertInteractionNavigationCompletedSafely({ + ... + }); + ... + } + metadata: + category: security + confidence: medium + technology: + - playwright + - browser + ghsa: GHSA-536Q-MJ95-H29H + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-536Q-MJ95-H29H + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-browser-interaction-navigation-guard + - id: ghsa-56f2-hvwg-5743.openclaw-unguarded-remote-media-fetch + message: Remote media fetch should go through the SSRF guard instead of calling fetch directly. + severity: ERROR + languages: + - typescript + patterns: + - pattern-either: + - pattern: | + export async function fetchRemoteMedia(...) { + ... + $RES = await $FETCHER($URL, ...); + ... + } + - pattern: | + async function fetchRemoteMedia(...) { + ... + $RES = await $FETCHER($URL, ...); + ... + } + - metavariable-regex: + metavariable: $URL + regex: (^url$|^requestUrl$|^mediaUrl$) + metadata: + cwe: CWE-918 + ghsa: GHSA-56F2-HVWG-5743 + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-56F2-HVWG-5743 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-unguarded-remote-media-fetch + - id: ghsa-5mx2-2mgw-x8rm.openclaw-webhook-loopback-passwordless-fallback + languages: + - typescript + severity: ERROR + message: Webhook auth falls back to passwordless targets on loopback instead of requiring configured secret matching. + pattern: | + const $MATCHING = + $STRICT.length > 0 + ? $STRICT + : isDirectLocalLoopbackRequest($REQ) + ? $PASSWORDLESS + : []; + metadata: + ghsa: GHSA-5MX2-2MGW-X8RM + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-5MX2-2MGW-X8RM + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-webhook-loopback-passwordless-fallback + - id: ghsa-5v6x-rfc3-7qfr.openclaw-system-run-approval-legacy-command-only-binding + message: system.run approval matching should bind to exact argv/canonical command data, not legacy command-only text. + languages: + - typescript + - javascript + severity: ERROR + patterns: + - pattern-inside: | + function $F(...){ + ... + } + - pattern: | + if (!$REQ.systemRunBinding) { + return $MISMATCH(...); + } + - pattern: | + return $MATCH({ + expected: $REQ.systemRunBinding, + actual: $ACTUAL.binding, + actualEnvKeys: $ACTUAL.envKeys, + }); + metadata: + confidence: medium + ghsa: GHSA-5V6X-RFC3-7QFR + detector: A + rationale: Requires v1 structured approval binding before matching. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-5V6X-RFC3-7QFR + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-system-run-approval-legacy-command-only-binding + - id: ghsa-5wcw-8jjv-m286.trusted-proxy-accept-without-origin-guard + languages: + - typescript + - javascript + message: Trusted-proxy authorization returns success without first applying authorizeTrustedProxyBrowserOrigin. + severity: ERROR + patterns: + - pattern: | + if ("user" in $RESULT) { + ... + return { ok: true, method: "trusted-proxy", user: $RESULT.user }; + } + - pattern-not: | + if ("user" in $RESULT) { + ... + const $ORIGIN_RESULT = authorizeTrustedProxyBrowserOrigin(...); + ... + if ($ORIGIN_RESULT) { + ... + } + ... + return { ok: true, method: "trusted-proxy", user: $RESULT.user }; + } + metadata: + category: security + confidence: medium + ghsa: GHSA-5WCW-8JJV-M286 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-5WCW-8JJV-M286 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: trusted-proxy-accept-without-origin-guard + - id: ghsa-5xfq-5mr7-426q.openclaw-session-transcript-path-traversal + message: Transcript path helper uses unvalidated sessionId or returns raw sessionFile without containment enforcement. + severity: WARNING + languages: + - typescript + pattern-either: + - pattern: "return candidate ? candidate : resolveSessionTranscriptPath(sessionId, opts?.agentId)" + - pattern: return path.join(resolveAgentSessionsDir(agentId), fileName) + metadata: + category: security + cwe: + - CWE-22 + ghsa: GHSA-5XFQ-5MR7-426Q + confidence: low + justification: Coverage-first detector for the vulnerable family in pre-fix session transcript path helpers. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-5XFQ-5MR7-426Q + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-session-transcript-path-traversal + - id: ghsa-62f6-mrcj-v8h5.runtime-overrides-unsanitized-override-store + languages: + - typescript + - javascript + severity: WARNING + message: Runtime override stores should sanitize object values before set-at-path writes to block prototype-reserved keys. + patterns: + - pattern-inside: | + function $F(..., $VALUE, ...) { + ... + } + - pattern: setConfigValueAtPath($TREE, $PATH, $VALUE) + - pattern-not: setConfigValueAtPath($TREE, $PATH, sanitizeOverrideValue($VALUE)) + - pattern-not: setConfigValueAtPath($TREE, $PATH, sanitizeInput($VALUE)) + - metavariable-regex: + metavariable: $F + regex: ^set.*Override$ + metadata: + ghsa: GHSA-62F6-MRCJ-V8H5 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-62F6-MRCJ-V8H5 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: runtime-overrides-unsanitized-override-store + - id: ghsa-65rx-fvh6-r4h2.openclaw-unquoted-heredoc-body-missing-expansion-token-guard + languages: + - typescript + severity: ERROR + message: Heredoc body parsing resets heredocLine after delimiter matching without checking hasUnquotedHeredocExpansionToken() for unquoted heredocs. Shell command substitution in heredoc bodies can bypass exec allowlist analysis. See GHSA-65RX-FVH6-R4H2. + metadata: + ghsa: GHSA-65RX-FVH6-R4H2 + category: security + cwe: + - CWE-78 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-65RX-FVH6-R4H2 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-unquoted-heredoc-body-missing-expansion-token-guard + paths: + include: + - src/infra/exec-approvals-analysis.ts + patterns: + - pattern: | + if (current) { + const line = current.stripTabs ? heredocLine.replace(/^\t+/, "") : heredocLine; + if (line === current.delimiter) { + pendingHeredocs.shift(); + } + } + heredocLine = ""; + - pattern-not: | + if (current) { + const line = current.stripTabs ? heredocLine.replace(/^\t+/, "") : heredocLine; + if (line === current.delimiter) { + pendingHeredocs.shift(); + } else if (!current.quoted && hasUnquotedHeredocExpansionToken(heredocLine)) { + return { ok: false, reason: "command substitution in unquoted heredoc", segments: [] }; + } + } + heredocLine = ""; + - id: ghsa-66r7-m7xm-v49h.qqbot-outbound-media-unvalidated-local-path + languages: + - typescript + - javascript + severity: ERROR + message: QQBot outbound helper resolves local media paths without media-root boundary validation. + patterns: + - pattern-either: + - pattern: | + const $MEDIA = resolveQQBotLocalMediaPath(normalizePath(...)); + - pattern: | + const $MEDIA = resolveQQBotLocalMediaPath($PATH); + - pattern-inside: | + export async function $FN(...){ + ... + } + - metavariable-regex: + metavariable: $FN + regex: ^(sendPhoto|sendVoice|sendVideoMsg|sendDocument|sendMedia|sendVoiceMessage)$ + - pattern-not-inside: | + const $RESOLVED = resolveOutboundMediaPath(...); + ... + metadata: + ghsa: GHSA-66R7-M7XM-V49H + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-66R7-M7XM-V49H + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: qqbot-outbound-media-unvalidated-local-path + - id: ghsa-6g25-pc82-vfwp.oauth-state-reuses-pkce-verifier + languages: + - typescript + - javascript + message: OAuth state should be generated independently, not reused from the PKCE verifier/codeVerifier. + severity: WARNING + patterns: + - pattern-either: + - pattern: | + function $F(..., $VERIFIER, ...) { + ... + new URLSearchParams({ ..., state: $VERIFIER, ... }) + ... + } + - pattern: | + function $F(..., $VERIFIER, ...) { + ... + const $P = new URLSearchParams(...); + ... + $P.set('state', $VERIFIER) + ... + } + - metavariable-regex: + metavariable: $VERIFIER + regex: (?i).*(verifier|codeVerifier).* + metadata: + ghsa: GHSA-6G25-PC82-VFWP + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-6G25-PC82-VFWP + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: oauth-state-reuses-pkce-verifier + - id: ghsa-6mqc-jqh6-x8fc.openclaw-canvas-auth-local-direct-bypass + message: Canvas/A2UI auth helper allows local-direct requests before bearer-or-capability auth. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-inside: | + async function $F(...){ + ... + } + - pattern-either: + - pattern: | + if (isLocalDirectRequest($REQ, ...)) { + return { ok: true }; + } + - pattern: | + const $LOCAL = isLocalDirectRequest($REQ, ...); + ... + if ($LOCAL) { + return { ok: true }; + } + metavariable-regex: + metavariable: $F + regex: authorize.* + metadata: + ghsa: GHSA-6MQC-JQH6-X8FC + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-6MQC-JQH6-X8FC + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-canvas-auth-local-direct-bypass + - id: ghsa-767m-xrhc-fxm7.telegram-target-writeback-admin-scope-required + message: Calls to maybePersistResolvedTelegramTarget should propagate gatewayClientScopes so the helper can enforce operator.admin before persisting Telegram target rewrites. + severity: ERROR + languages: + - typescript + patterns: + - pattern: | + maybePersistResolvedTelegramTarget({ + ..., + rawTarget: $RAW, + ..., + resolvedChatId: $CHAT, + ..., + }) + - pattern-not: | + maybePersistResolvedTelegramTarget({ + ..., + gatewayClientScopes: $SCOPES, + ..., + }) + metadata: + ghsa: GHSA-767M-XRHC-FXM7 + detector: A + rationale: Structural callsite check for helper invocations that omit gatewayClientScopes, which disables the operator.admin gate inside Telegram target writeback persistence. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-767M-XRHC-FXM7 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: telegram-target-writeback-admin-scope-required + - id: ghsa-77w2-crqv-cmv3.feishu-structured-card-action-envelope-required + languages: + - typescript + - javascript + severity: ERROR + message: Feishu interactive card buttons that trigger commands or text should use a structured interaction envelope instead of raw text/command callback values. + patterns: + - pattern-either: + - pattern: | + { + ..., + tag: "button", + ..., + value: { command: $CMD, ... }, + ..., + } + - pattern: | + { + ..., + tag: "button", + ..., + value: { text: $TXT, ... }, + ..., + } + paths: + include: + - "**/*.ts" + - "**/*.js" + metadata: + ghsa: GHSA-77W2-CRQV-CMV3 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-77W2-CRQV-CMV3 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: feishu-structured-card-action-envelope-required + - id: ghsa-782p-5fr5-7fj8.channel-metadata-into-trusted-system-prompt + languages: + - typescript + severity: WARNING + message: Untrusted channel metadata is being interpolated into trusted GroupSystemPrompt parts. Route it through UntrustedContext instead. + pattern-either: + - pattern: | + const $SYSTEM_PROMPT_PARTS = [ + ..., + $CHANNEL_DESC ? `Channel description: ${$CHANNEL_DESC}` : null, + ..., + ].filter(($ENTRY) => Boolean($ENTRY)); + - pattern: | + const $SYSTEM_PROMPT_PARTS = [ + ..., + $CHANNEL_DESC ? `Channel topic: ${$CHANNEL_DESC}` : null, + ..., + ].filter(($ENTRY) => Boolean($ENTRY)); + - pattern: | + const $SYSTEM_PROMPT_PARTS = [ + ..., + $CHANNEL_DESC ? `Forum topic: ${$CHANNEL_DESC}` : null, + ..., + ].filter(($ENTRY) => Boolean($ENTRY)); + metadata: + ghsa: GHSA-782P-5FR5-7FJ8 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-782P-5FR5-7FJ8 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: channel-metadata-into-trusted-system-prompt + - id: ghsa-796m-2973-wc5q.env-wrapper-transparent-unwrap + languages: + - typescript + - javascript + severity: WARNING + message: env dispatch-wrapper handling unwraps env invocations without a semantic-modifier guard. Review whether env assignments or env -S/--split-string can change runtime behavior and bypass policy expectations. + patterns: + - pattern-either: + - pattern: | + case "env": + return unwrapDispatchWrapper($WRAPPER, unwrapEnvInvocation($ARGV)); + - pattern: | + if ($WRAP == "env") { + return unwrapEnvInvocation($ARGV); + } + metadata: + ghsa: GHSA-796M-2973-WC5Q + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-796M-2973-WC5Q + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: env-wrapper-transparent-unwrap + - id: ghsa-7fcc-cw49-xm78.windows-shell-fallback-after-spawn-error + message: Windows spawn retry logic that reruns the same operation with shell=true after ENOENT/EINVAL wrapper failures can turn argv into shell-interpreted command text. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + if (process.platform === "win32" && ($ERR.code === "EINVAL" || $ERR.code === "ENOENT")) { + ... + return await $RUN(..., true); + } + - pattern: | + if (process.platform === "win32" && ($ERR.code === "ENOENT" || $ERR.code === "EINVAL")) { + ... + return await $RUN(..., true); + } + - pattern: | + if (process.platform === "win32" && $CHECK($ERR)) { + ... + return await $RUN(..., true); + } + - pattern: | + if (process.platform === "win32" && $RETRYABLE) { + ... + return await $RUN(..., true); + } + - pattern: | + if (process.platform === "win32") { + ... + if ($CHECK($ERR)) { + ... + return await $RUN(..., true); + } + } + - pattern-either: + - pattern-inside: | + function $CHECK($ERR) { + ... + return $CODE === "EINVAL" || $CODE === "ENOENT"; + } + ... + - pattern-inside: | + function $CHECK($ERR) { + ... + return $CODE === "ENOENT" || $CODE === "EINVAL"; + } + ... + - pattern-inside: | + const $RETRYABLE = $ERR && ($ERR.code === "ENOENT" || $ERR.code === "EINVAL"); + ... + - pattern-inside: | + const $RETRYABLE = $ERR && ($ERR.code === "EINVAL" || $ERR.code === "ENOENT"); + ... + metadata: + ghsa: GHSA-7FCC-CW49-XM78 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-7FCC-CW49-XM78 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: windows-shell-fallback-after-spawn-error + - id: ghsa-7ggg-pvrf-458v.openclaw-host-exec-python-index-override + message: Request-scoped host exec env overrides include Python package index redirect variables (PIP_*_INDEX, UV_*_INDEX) that can reroute package downloads. + languages: + - typescript + - javascript + severity: ERROR + patterns: + - pattern-either: + - pattern: | + sanitizeHostExecEnv({ + ..., + overrides: { + ..., + PIP_INDEX_URL: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnv({ + ..., + overrides: { + ..., + PIP_PYPI_URL: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnv({ + ..., + overrides: { + ..., + PIP_EXTRA_INDEX_URL: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnv({ + ..., + overrides: { + ..., + UV_INDEX: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnv({ + ..., + overrides: { + ..., + UV_INDEX_URL: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnv({ + ..., + overrides: { + ..., + UV_DEFAULT_INDEX: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnv({ + ..., + overrides: { + ..., + UV_EXTRA_INDEX_URL: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnvWithDiagnostics({ + ..., + overrides: { + ..., + PIP_INDEX_URL: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnvWithDiagnostics({ + ..., + overrides: { + ..., + PIP_PYPI_URL: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnvWithDiagnostics({ + ..., + overrides: { + ..., + PIP_EXTRA_INDEX_URL: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnvWithDiagnostics({ + ..., + overrides: { + ..., + UV_INDEX: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnvWithDiagnostics({ + ..., + overrides: { + ..., + UV_INDEX_URL: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnvWithDiagnostics({ + ..., + overrides: { + ..., + UV_DEFAULT_INDEX: $VALUE, + ... + }, + ... + }) + - pattern: | + sanitizeHostExecEnvWithDiagnostics({ + ..., + overrides: { + ..., + UV_EXTRA_INDEX_URL: $VALUE, + ... + }, + ... + }) + metadata: + ghsa: GHSA-7GGG-PVRF-458V + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-7GGG-PVRF-458V + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-host-exec-python-index-override + - id: ghsa-7h7g-x2px-94hj.openclaw-setup-bootstrap-issued-with-profile-argument + message: Pairing setup code should issue bootstrap tokens with a dedicated profile object, not ad hoc role/scopes arguments. + severity: WARNING + languages: + - typescript + patterns: + - pattern-either: + - pattern: | + return { + ..., + payload: { + ..., + bootstrapToken: (await issueDeviceBootstrapToken({ + baseDir: $DIR, + role: $ROLE, + scopes: $SCOPES, + })).token, + ... + }, + ... + } + - pattern: | + $ISSUED = await issueDeviceBootstrapToken({ + baseDir: $DIR, + role: $ROLE, + scopes: $SCOPES, + }) + ... + return { ..., payload: { ..., bootstrapToken: $ISSUED.token, ... }, ... } + metadata: + ghsa: GHSA-7H7G-X2PX-94HJ + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-7H7G-X2PX-94HJ + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-setup-bootstrap-issued-with-profile-argument + - id: ghsa-7jp6-r74r-995q.openclaw-matrix-set-profile-missing-owner-guard + languages: + - typescript + severity: WARNING + message: Matrix set-profile action dispatches without checking senderIsOwner. See GHSA-7JP6-R74R-995Q. + metadata: + ghsa: GHSA-7JP6-R74R-995Q + category: security + cwe: + - CWE-862 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-7JP6-R74R-995Q + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-matrix-set-profile-missing-owner-guard + paths: + include: + - extensions/matrix/src/actions.ts + - src/agents/tools/matrix-actions.ts + patterns: + - pattern: | + dispatch({ + action: "setProfile", + ... + }) + - pattern-not-inside: | + if (ctx.senderIsOwner !== true) { + ... + } + ... + - id: ghsa-7qf6-h84j-8fq4.msteams-safe-fetch-missing-resolvefn + languages: + - typescript + severity: ERROR + message: MS Teams attachment callers that rely on safeFetch/safeFetchWithPolicy should pass resolveFn so each hostname/redirect hop gets DNS/IP validation instead of only hostname allowlist checks. + patterns: + - pattern-either: + - pattern: | + safeFetch({ + ... + }) + - pattern: | + safeFetchWithPolicy({ + ... + }) + - pattern-not: | + safeFetch({ + ..., + resolveFn: $RESOLVER, + ..., + }) + - pattern-not: | + safeFetchWithPolicy({ + ..., + resolveFn: $RESOLVER, + ..., + }) + - pattern-not-inside: | + describe(...) + - pattern-not-inside: | + it(...) + - pattern-not-inside: | + test(...) + - pattern-not-inside: | + expect(...) + metadata: + ghsa: GHSA-7QF6-H84J-8FQ4 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-7QF6-H84J-8FQ4 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: msteams-safe-fetch-missing-resolvefn + - id: ghsa-7rcp-mxpq-72pj.oauth-manual-callback-code-only-acceptance + message: Manual OAuth callback parsing should not accept bare authorization codes because that bypasses CSRF state validation. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern: | + catch { + ... + return { code: $CODE, state: $EXPECTED }; + } + metadata: + category: security + cwe: CWE-352 + ghsa: GHSA-7RCP-MXPQ-72PJ + rationale: Detects catch blocks in OAuth callback parsers that fabricate a matching state while accepting non-URL code-only input. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-7RCP-MXPQ-72PJ + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: oauth-manual-callback-code-only-acceptance + - id: ghsa-7wv4-cc7p-jhxc.openclaw.workspace-dotenv-runtime-guard-incomplete + languages: + - typescript + - javascript + severity: WARNING + message: Workspace .env runtime-control blocklist looks incomplete. Guard helpers should block OPENCLAW_GATEWAY_URL/PORT, OPENCLAW_UPDATE_*, OPENCLAW_SKIP_*, and ClawHub/browser-control variables, not just auth keys and _BASE_URL redirects. + patterns: + - pattern-either: + - pattern: | + const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([...]); + - pattern: | + const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([...,]); + - pattern-not-regex: OPENCLAW_GATEWAY_URL + - pattern-not-regex: OPENCLAW_GATEWAY_PORT + - pattern-not-regex: OPENCLAW_BROWSER_EXECUTABLE_PATH + - pattern-not-regex: OPENCLAW_SKIP_ + - pattern-not-regex: OPENCLAW_UPDATE_ + - pattern-not-regex: OPENCLAW_CLAWHUB_ + - pattern-not-regex: CLAWHUB_ + metadata: + ghsa: GHSA-7WV4-CC7P-JHXC + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-7WV4-CC7P-JHXC + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.workspace-dotenv-runtime-guard-incomplete + - id: ghsa-7xmq-g46g-f8pv.openclaw-sandbox-media-deferred-path-read + message: Sandbox media flows should use root-scoped read helpers at use time instead of deferring to raw bridge/fs pathname reads. + severity: WARNING + languages: + - typescript + - javascript + metadata: + category: security + ghsa: GHSA-7XMQ-G46G-F8PV + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-7XMQ-G46G-F8PV + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-sandbox-media-deferred-path-read + pattern-either: + - patterns: + - pattern: | + $RES = await resolveSandboxedBridgeMediaPath(...); + ... + await loadWebMedia($RES.resolved, { + ..., + sandboxValidated: true, + readFile: (filePath) => $BRIDGE.readFile({ filePath, cwd: $ROOT }), + ..., + }); + - pattern-not: | + $RES = await resolveSandboxedBridgeMediaPath(...); + ... + await loadWebMedia($RES.resolved, { + ..., + sandboxValidated: true, + readFile: createSandboxBridgeReadFile(...), + ..., + }); + - patterns: + - pattern: | + return { + ..., + sandboxValidated: true, + readFile: (filePath: string) => fs.readFile(filePath), + ..., + }; + - pattern-not: | + return { + ..., + sandboxValidated: true, + readFile: createRootScopedReadFile(...), + ..., + }; + - id: ghsa-82g8-464f-2mv7.openclaw-skill-env-host-injection + languages: + - typescript + - javascript + message: Skill env overrides are copied into process.env without host-env safety sanitization. + severity: ERROR + patterns: + - pattern-either: + - pattern: | + for (const [$KEY, $VALUE] of Object.entries($ENV)) { + ... + process.env[$KEY] = $VALUE; + ... + } + - pattern: | + for (const [$KEY, $VALUE] of Object.entries($ENV)) { + ... + $UPDATES.push({ key: $KEY, ... }); + ... + process.env[$KEY] = $VALUE; + ... + } + - pattern-not-inside: | + const $SANITIZED = sanitizeSkillEnvOverrides(...); + ... + - pattern-not-inside: | + const $SANITIZED = sanitizeEnvVars(...); + ... + metadata: + ghsa: GHSA-82G8-464F-2MV7 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-82G8-464F-2MV7 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-skill-env-host-injection + - id: ghsa-846p-hgpv-vphc.qqbot-outbound-local-read-without-boundary-check + message: QQ Bot outbound local media handling reads a user-influenced path after resolveQQBotLocalMediaPath() without resolveOutboundMediaPath()/resolveQQBotPayloadLocalFilePath() boundary enforcement. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + $MP = resolveQQBotLocalMediaPath(normalizePath($RAW)); + ... + readFileAsync($MP) + - pattern: | + $MP = resolveQQBotLocalMediaPath(normalizePath($RAW)); + ... + audioFileToSilkBase64($MP, ...) + - pattern: | + $MP = resolveQQBotLocalMediaPath(normalizePath($RAW)); + ... + checkFileSize($MP) + - pattern-not: | + $RES = resolveOutboundMediaPath($RAW, ..., ...) + ... + - pattern-not: | + $SAFE = resolveQQBotPayloadLocalFilePath($MP) + ... + - pattern-not: | + if (!resolveQQBotPayloadLocalFilePath($MP)) { + ... + } + metadata: + category: security + technology: + - qqbot + confidence: medium + ghsa: GHSA-846P-HGPV-VPHC + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-846P-HGPV-VPHC + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: qqbot-outbound-local-read-without-boundary-check + - id: ghsa-8689-gm9g-jgr6.plivo-v3-replay-key-uses-unsorted-url + languages: + - typescript + - javascript + severity: ERROR + message: Plivo V3 replay keys should be derived from the same canonicalized URL+params base used for signature verification, not from the raw verification URL string. + patterns: + - pattern-inside: | + if ($SIG && $NONCE) { + ... + } + - pattern-either: + - pattern: | + $REPLAY = `plivo:v3:${sha256Hex(`${$URL}\n${$NONCE}`)}` + - pattern: | + $REPLAY = "plivo:v3:" + sha256Hex($URL + "\n" + $NONCE) + - pattern: | + $REPLAY = "plivo:v3:" + sha256Hex(`${$URL}\n${$NONCE}`) + - metavariable-regex: + metavariable: $URL + regex: .*(url|verificationUrl|requestUrl|webhookUrl).* + metadata: + category: security + confidence: medium + ghsa: GHSA-8689-GM9G-JGR6 + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-8689-GM9G-JGR6 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: plivo-v3-replay-key-uses-unsorted-url + - id: ghsa-8cp7-rp8r-mg77.ipv6-transition-special-use-ssrf-guard-bypass + message: Security-sensitive hostname/IP guard composes isBlockedHostname with isPrivateIpAddress instead of routing through the shared isBlockedHostnameOrIp classifier. Review for missed IPv6 transition encodings such as ISATAP, NAT64, 6to4, or Teredo. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern: isBlockedHostname($HOST) + - pattern-not: isBlockedHostnameOrIp($HOST) + - pattern-not-inside: | + function canFetch(...) { + ... + } + metadata: + cwe: CWE-918 + ghsa: GHSA-8CP7-RP8R-MG77 + detector_kind: reusable-opengrep + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-8CP7-RP8R-MG77 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: ipv6-transition-special-use-ssrf-guard-bypass + - id: ghsa-8fmp-37rc-p5g7.openclaw-service-env-merges-unfiltered-dotenv-or-config-env + languages: + - typescript + - javascript + severity: WARNING + message: Untrusted config/.env environment variables are merged directly into a managed service environment. Filter startup-control keys (for example NODE_OPTIONS, LD_*, DYLD_*) before embedding them in LaunchAgent/systemd/Scheduled Task environments. + metadata: + category: security + ghsa: GHSA-8FMP-37RC-P5G7 + cwe: CWE-15 + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-8FMP-37RC-P5G7 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-service-env-merges-unfiltered-dotenv-or-config-env + patterns: + - pattern-either: + - pattern: | + const $ENV = { + ...$SOURCE, + ...$REST + }; + - pattern: | + const $ENV = { + ...$PREFIX, + ...$SOURCE, + ...$REST + }; + - metavariable-pattern: + metavariable: $SOURCE + patterns: + - pattern-either: + - pattern: readStateDirDotEnvVars(...) + - pattern: collectConfigEnvVars(...) + - pattern: collectConfigServiceEnvVars(...) + - pattern: collectConfigRuntimeEnvVars(...) + - pattern-not-inside: | + const $SANITIZED = sanitizeHostExecEnvWithDiagnostics(...); + ... + - pattern-not-inside: | + const $SAFE = sanitizeServiceEnvVars(...); + ... + - id: ghsa-8jpq-5h99-ff5r.ghsa-8jpq-local-path-media-send + languages: + - typescript + - javascript + severity: ERROR + message: Media-send code should not use a local-path classifier to read attacker-controlled paths directly from disk while fetching the same input in the remote branch. + patterns: + - pattern-either: + - pattern: | + if ($ISLOCAL($INPUT)) { + ... + return $FS.readFileSync($PATH); + } else { + ... + $RESPONSE = await fetch($INPUT, ...); + ... + } + - pattern: | + if ($ISLOCAL($INPUT)) { + ... + $DATA = $FS.readFileSync($PATH); + ... + } else { + ... + $RESPONSE = await fetch($INPUT, ...); + ... + } + - pattern: | + if ($ISLOCAL($INPUT)) { + ... + return await $FS.readFile($PATH); + } + ... + $RESPONSE = await fetch($INPUT, ...); + ... + - pattern: | + if ($ISLOCAL($INPUT)) { + ... + $DATA = await $FS.readFile($PATH); + ... + } + ... + $RESPONSE = await fetch($INPUT, ...); + ... + metadata: + ghsa: GHSA-8JPQ-5H99-FF5R + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-8JPQ-5H99-FF5R + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: ghsa-8jpq-local-path-media-send + - id: ghsa-8mf7-vv8w-hjr2.openclaw.safebin-profile-lookup-permissive-fallback + languages: + - typescript + - javascript + severity: ERROR + message: | + A safe-bin profile lookup is using a `??` or `||` permissive fallback. The lookup must fail closed: if the binary is missing a profile, deny the call (require user approval) — not allow it with a generic profile. See GHSA-8MF7-VV8W-HJR2. + metadata: + category: security + cwe: + - CWE-276 + - CWE-77 + ghsas: + - GHSA-8MF7-VV8W-HJR2 + ghsa: GHSA-8MF7-VV8W-HJR2 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-8MF7-VV8W-HJR2 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.safebin-profile-lookup-permissive-fallback + paths: + include: + - src/infra/exec-safe-bin-policy*.ts + - src/infra/exec-approvals*.ts + - src/infra/exec-allowlist*.ts + - src/agents/bash-tools.exec*.ts + pattern-either: + - patterns: + - pattern-either: + - pattern: $PROFILES[$NAME] ?? $FALLBACK + - pattern: $PROFILES[$NAME] || $FALLBACK + - metavariable-regex: + metavariable: $PROFILES + regex: ^(safeBinProfiles|SAFE_BIN_PROFILES|profiles)$ + - patterns: + - pattern: $X.$PROFILES[$NAME] ?? $FALLBACK + - metavariable-regex: + metavariable: $PROFILES + regex: ^(safeBinProfiles|SAFE_BIN_PROFILES|profiles)$ + - id: ghsa-92jp-89mq-4374.openclaw-browser-bridge-preauth-helper-route + message: Privileged browser bridge helper route is registered before auth middleware, so helper access can bypass bridge auth. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-inside: | + export async function $F(...){ + ... + } + - pattern: | + app.get($ROUTE, ...) + - metavariable-regex: + metavariable: $ROUTE + regex: ^["'`](?:/sandbox/novnc|/[^"'`]*(?:helper|observer|novnc)[^"'`]*)["'`]$ + - pattern-not-inside: | + export async function $F(...){ + ... + installBrowserAuthMiddleware(app, ...) + ... + app.get($ROUTE, ...) + ... + } + - pattern-not: | + app.get($ROUTE, ($REQ, $RES) => { + if (!hasVerifiedBrowserAuth($REQ)) { + ... + } + ... + }) + metadata: + category: security + technology: + - express + confidence: medium + ghsa: GHSA-92JP-89MQ-4374 + rationale: Helper routes like noVNC observer/token redemption should not be reachable before bridge auth is established. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-92JP-89MQ-4374 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-browser-bridge-preauth-helper-route + - id: ghsa-94pw-c6m8-p9p9.openclaw-gateway-config-mutation-guard-missing-dangerous-flag-diff + languages: + - typescript + severity: ERROR + message: assertGatewayConfigMutationAllowed() does not compare dangerous config flags using collectEnabledInsecureOrDangerousFlags(). Gateway config.apply/config.patch can otherwise enable dangerous settings through a write-scoped gateway tool. See GHSA-94PW-C6M8-P9P9. + metadata: + ghsa: GHSA-94PW-C6M8-P9P9 + category: security + cwe: + - CWE-266 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-94PW-C6M8-P9P9 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-gateway-config-mutation-guard-missing-dangerous-flag-diff + paths: + include: + - src/agents/tools/gateway-tool.ts + patterns: + - pattern: | + function assertGatewayConfigMutationAllowed($PARAMS: $TYPE) { + ... + } + - pattern-not: | + function assertGatewayConfigMutationAllowed($PARAMS: $TYPE) { + ... + collectEnabledInsecureOrDangerousFlags(...) + ... + } + - id: ghsa-9528-x887-j2fp.openclaw-webhook-signature-missing-auth-rate-limit + languages: + - typescript + severity: WARNING + message: Shared-secret webhook verification does not record auth-rate-limit failure on invalid signature. See GHSA-9528-X887-J2FP. + metadata: + ghsa: GHSA-9528-X887-J2FP + category: security + cwe: + - CWE-307 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-9528-X887-J2FP + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-webhook-signature-missing-auth-rate-limit + paths: + include: + - extensions/nextcloud-talk/src/monitor.ts + - extensions/**/src/**/monitor*.ts + patterns: + - pattern: verifyNextcloudTalkSignature(...) + - pattern-not-inside: | + ... + verifyNextcloudTalkSignature(...) + ... + $LIM.recordFailure(...) + ... + - id: ghsa-98ch-45wp-ch47.openclaw.system-run-approval-binding-portable-normalization-regression + languages: + - typescript + severity: ERROR + message: normalizeEnvVarKey with portable:true in system-run-approval-binding.ts instead of normalizeHostOverrideEnvVarKey causes approval-integrity gap. See GHSA-98CH-45WP-CH47. + metadata: + category: security + cwe: + - CWE-178 + ghsas: + - GHSA-98CH-45WP-CH47 + ghsa: GHSA-98CH-45WP-CH47 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-98CH-45WP-CH47 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.system-run-approval-binding-portable-normalization-regression + paths: + include: + - src/infra/system-run-approval-binding.ts + pattern: "normalizeEnvVarKey($KEY, { portable: true })" + - id: ghsa-98hh-7ghg-x6rq.approval-resolve-call-review + languages: + - typescript + - javascript + message: Review approval resolution call sites for missing channel-specific approver authorization. + severity: WARNING + pattern-either: + - pattern: | + await callApprovalMethod("exec.approval.resolve"); + - pattern: | + await callApprovalMethod("plugin.approval.resolve"); + metadata: + category: security + ghsa: GHSA-98HH-7GHG-X6RQ + detector: A + note: Reusable review surface for approval-resolution entrypoints; authorization must be verified manually. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-98HH-7GHG-X6RQ + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: approval-resolve-call-review + - id: ghsa-9jpj-g8vv-j5mf.oauth-state-reuses-pkce-verifier + languages: + - typescript + - javascript + severity: ERROR + message: OAuth state should be generated independently and must not reuse PKCE verifier material. + patterns: + - pattern-either: + - pattern: | + new URLSearchParams({ + ..., + state: $PKCE.verifier, + ..., + }) + - pattern: | + new URLSearchParams({ + ..., + state: verifier, + ..., + }) + - pattern: | + buildAuthUrl(..., $PKCE.verifier, ...) + - pattern: | + buildAuthUrl(..., verifier, ...) + metadata: + category: security + confidence: medium + rationale: Detects OAuth flows that directly reuse PKCE verifier material as the OAuth state token. + ghsa: GHSA-9JPJ-G8VV-J5MF + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-9JPJ-G8VV-J5MF + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: oauth-state-reuses-pkce-verifier + - id: ghsa-9q7v-8mr7-g23p.openclaw.marketplace-plugin-download-unguarded-fetch + languages: + - typescript + severity: WARNING + message: | + In src/plugins/marketplace.ts, a raw fetch() is used for a marketplace archive URL without going through fetchWithSsrFGuard. A compromised marketplace server could return a URL pointing to internal network services (RFC1918, cloud metadata). Use fetchWithSsrFGuard({ url: archiveUrl, auditContext: "marketplace-plugin-download" }). See GHSA-9Q7V-8MR7-G23P. + metadata: + category: security + cwe: + - CWE-918 + ghsas: + - GHSA-9Q7V-8MR7-G23P + ghsa: GHSA-9Q7V-8MR7-G23P + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-9Q7V-8MR7-G23P + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.marketplace-plugin-download-unguarded-fetch + paths: + include: + - src/plugins/marketplace.ts + - src/plugins/marketplace-*.ts + - src/plugins/plugin-download*.ts + patterns: + - pattern: fetch($URL, ...) + - pattern-not: fetchWithSsrFGuard(...) + - pattern-not-inside: | + fetchWithSsrFGuard(...) + - id: ghsa-9wqx-g2cw-vc7r.matrix-verification-notice-missing-dm-access-gate + message: sendVerificationNotice is called from a Matrix verification file that does not import resolveMatrixMonitorAccessState — the DM access policy check needed to gate verification notices is missing. Verification notices must be gated on DM access policy or they leak the bot's verification state to non-allowed peers. See GHSA-9WQX-G2CW-VC7R. + severity: ERROR + languages: + - typescript + metadata: + ghsa: GHSA-9WQX-G2CW-VC7R + category: authorization + cwe: + - CWE-863 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-9WQX-G2CW-VC7R + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: matrix-verification-notice-missing-dm-access-gate + paths: + include: + - extensions/matrix/src/matrix/monitor/verification-*.ts + patterns: + - pattern: sendVerificationNotice(...) + - pattern-not-inside: | + import { ..., resolveMatrixMonitorAccessState, ... } from "$X"; + ... + - pattern-not-inside: | + import { resolveMatrixMonitorAccessState } from "$X"; + ... + - pattern-not-inside: | + import { resolveMatrixMonitorAccessState, ... } from "$X"; + ... + - id: ghsa-cg6c-q2hx-69h7.plivo-v2-replay-key-uses-full-verification-url + message: Plivo V2 replay keys should be derived from the signed base URL without the query string; hashing the full verification URL lets query-only variants mint fresh verifiedRequestKey values. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + const $KEY = `plivo:v2:${sha256Hex(`${$URL}\n${$NONCE}`)}` + - pattern: | + const $KEY = "plivo:v2:" + $HASH(`${$URL}\n${$NONCE}`) + - metavariable-regex: + metavariable: $URL + regex: ^(verificationUrl|url)$ + metadata: + ghsa: GHSA-CG6C-Q2HX-69H7 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-CG6C-Q2HX-69H7 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: plivo-v2-replay-key-uses-full-verification-url + - id: ghsa-chm2-m3w2-wcxm.googlechat-users-email-allowlist-mutable-principal + languages: + - typescript + - javascript + severity: WARNING + message: Google Chat allowlists should not treat users/ entries as email matches; that mixes a mutable email principal into a users/ identity slot. + patterns: + - pattern-either: + - pattern: | + $EMAIL && $ENTRY.replace(/^users\//i, "") === $EMAIL + - pattern: | + $ENTRY.startsWith("users/") && $EMAIL && $ENTRY.slice("users/".length) === $EMAIL + - metavariable-regex: + metavariable: $EMAIL + regex: .*[Ee]mail.* + metadata: + ghsa: GHSA-CHM2-M3W2-WCXM + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-CHM2-M3W2-WCXM + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: googlechat-users-email-allowlist-mutable-principal + - id: ghsa-cv7m-c9jx-vg7q.openclaw-browser-upload-raw-paths-forwarded + message: Browser upload/file-chooser code forwards caller-controlled `paths` directly. Upload paths must first be resolved under DEFAULT_UPLOAD_DIR and then forwarded as validated/resolved paths. Raw paths can read arbitrary local files from the Gateway host. See GHSA-CV7M-C9JX-VG7Q. + languages: + - typescript + severity: ERROR + metadata: + ghsa: GHSA-CV7M-C9JX-VG7Q + category: security + cwe: + - CWE-22 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-CV7M-C9JX-VG7Q + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-browser-upload-raw-paths-forwarded + paths: + include: + - src/agents/tools/browser-tool.ts + - src/browser/routes/agent.act.ts + - extensions/browser/src/browser/routes/**/*.ts + - extensions/browser/src/browser/pw-tools-core.interactions.ts + pattern-regex: ^\s*paths\s*,\s*$ + - id: ghsa-cwf8-44x6-32c2.openclaw-openshell-uploadpath-direct-localpath-upload + languages: + - typescript + severity: ERROR + message: uploadPathToRemote() passes localPath directly to `openshell sandbox upload`. Uploads must use a symlink-free staged snapshot (tmpDir from stageDirectoryContents) before syncing into the remote sandbox. See GHSA-CWF8-44X6-32C2. + metadata: + ghsa: GHSA-CWF8-44X6-32C2 + category: security + cwe: + - CWE-22 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-CWF8-44X6-32C2 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-openshell-uploadpath-direct-localpath-upload + paths: + include: + - extensions/openshell/src/backend.ts + pattern-regex: private async uploadPathToRemote[\s\S]*?"sandbox",[\s\S]*?"upload",[\s\S]*?localPath,[\s\S]*?remotePath, + - id: ghsa-cwq8-6f96-g3q4.openclaw-install-scan-fail-open + languages: + - typescript + - javascript + severity: ERROR + message: Install flow catches install-source scan failures and continues into an install/write path instead of blocking. + metadata: + category: security + cwe: + - CWE-636 + - CWE-754 + ghsa: GHSA-CWQ8-6F96-G3Q4 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-CWQ8-6F96-G3Q4 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-install-scan-fail-open + patterns: + - pattern-either: + - pattern: | + try { + ... + const $SCAN_RESULT = await $SCAN(...); + ... + } catch ($ERR) { + ... + $LOGGER.warn(...); + ... + } + ... + return await $INSTALL(...); + - pattern: | + try { + ... + await $SCAN(...); + ... + } catch ($ERR) { + ... + $LOGGER.warn(...); + ... + } + ... + return await $INSTALL(...); + - pattern: | + try { + ... + const $SCAN_RESULT = await $SCAN(...); + ... + } catch ($ERR) { + ... + $LOGGER.warn(...); + ... + } + ... + return $INSTALL(...); + - pattern: | + try { + ... + await $SCAN(...); + ... + } catch ($ERR) { + ... + $LOGGER.warn(...); + ... + } + ... + return $INSTALL(...); + - metavariable-regex: + metavariable: $SCAN + regex: ^(runtime\.)?scan[A-Za-z0-9_]*InstallSource(Runtime)?$ + - metavariable-regex: + metavariable: $INSTALL + regex: ^(install|write|copy|proceed)[A-Za-z0-9_]* + - id: ghsa-cxpw-2g23-2vgw.acp-extract-text-from-prompt-without-size-limit + languages: + - typescript + message: ACP prompt handlers should pass an explicit byte limit into extractTextFromPrompt before forwarding the assembled message to chat.send. + severity: ERROR + pattern-regex: (?s)async\s+prompt\s*\([^)]*\)\s*[:{][\s\S]*?extractTextFromPrompt\s*\([^,)]*\)(?!\s*,) + metadata: + ghsa: GHSA-CXPW-2G23-2VGW + cwe: + - CWE-20 + - CWE-400 + category: security + technology: + - openclaw + - acp + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-CXPW-2G23-2VGW + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: acp-extract-text-from-prompt-without-size-limit + - id: ghsa-f5mf-3r52-r83w.zalouser-dangerous-group-name-auth-match + message: Group authorization candidates include mutable group name or slug values without an explicit opt-in, which can let authorization depend on attacker-controlled naming instead of stable ids. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + buildZalouserGroupCandidates({ + ..., + groupName: $GROUP_NAME, + ..., + }) + - pattern: | + buildZalouserGroupCandidates({ + ..., + groupChannel: $GROUP_CHANNEL, + ..., + }) + - pattern-not: | + buildZalouserGroupCandidates({ + ..., + allowNameMatching: $ALLOW, + ..., + }) + - pattern-not-inside: | + if (isZalouserDangerousNameMatchingEnabled(...)) { + ... + } + - pattern-not-inside: | + const $FLAG = isZalouserDangerousNameMatchingEnabled(...); + ... + buildZalouserGroupCandidates({ + ..., + allowNameMatching: $FLAG, + ..., + }) + metadata: + ghsa: GHSA-F5MF-3R52-R83W + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-F5MF-3R52-R83W + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: zalouser-dangerous-group-name-auth-match + - id: ghsa-f7fh-qg34-x2xh.openclaw-direct-cdp-websocket-top-level-branch-missing-validation + languages: + - typescript + - javascript + severity: ERROR + message: createTargetViaCdp() has a top-level direct WebSocket branch that assigns opts.cdpUrl to wsUrl without validating it first. Direct CDP ws:// targets must be checked with assertCdpEndpointAllowed() to prevent second-hop SSRF. See GHSA-F7FH-QG34-X2XH. + metadata: + ghsa: GHSA-F7FH-QG34-X2XH + category: security + cwe: + - CWE-918 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-F7FH-QG34-X2XH + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-direct-cdp-websocket-top-level-branch-missing-validation + paths: + include: + - extensions/browser/src/browser/cdp.ts + pattern-regex: (?m)^\s*if\s*\(isWebSocketUrl\(opts\.cdpUrl\)\)\s*\{(?:\n\s*//[^\n]*)?\n\s*wsUrl\s*=\s*opts\.cdpUrl\s*; + - id: ghsa-fg3m-vhrr-8gj6.windows-shell-fallback-retry + languages: + - typescript + - javascript + severity: ERROR + message: Windows retry logic that converts wrapper-launch errors into shell execution can reinterpret argv via cmd.exe. Prefer fail-closed wrapper resolution. + patterns: + - pattern-either: + - pattern: | + if (process.platform === "win32" && $CHECK($ERR)) { + ... + return await $RUN(..., true); + } + - pattern: | + if (process.platform === "win32" && $CODE && $SET.has($CODE)) { + ... + return await $RUN(..., true); + } + metadata: + ghsa: GHSA-FG3M-VHRR-8GJ6 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-FG3M-VHRR-8GJ6 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: windows-shell-fallback-retry + - id: ghsa-fh3f-q9qw-93j9.sandbox-config-sha1-hash + message: Security-sensitive sandbox/config hash helper uses deprecated SHA-1. + severity: WARNING + languages: + - typescript + - javascript + patterns: + - pattern: $CRYPTO.createHash("sha1") + - pattern-either: + - pattern-inside: | + function $F(...){ + ... + $PAYLOAD = JSON.stringify(...); + ... + return ...; + } + - pattern-inside: | + const $F = (...) => { + ... + $PAYLOAD = JSON.stringify(...); + ... + return ...; + } + metadata: + category: security + confidence: medium + technology: + - nodejs + ghsa: GHSA-FH3F-Q9QW-93J9 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-FH3F-Q9QW-93J9 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: sandbox-config-sha1-hash + - id: ghsa-fqcm-97m6-w7rm.openclaw-unconditional-sandboxvalidated-loadwebmedia + languages: + - typescript + severity: WARNING + message: "loadWebMedia is running with sandboxValidated: true and a direct readFile override without an adjacent sandboxRoot-based fallback to localRoots. This can bypass local media root checks when sandboxRoot is unset." + patterns: + - pattern-inside: | + async function $F(...){ + ... + } + - pattern: | + loadWebMedia($SOURCE, { + ..., + sandboxValidated: true, + ..., + readFile: ($FILE) => $READ, + ..., + }) + - pattern-not-inside: | + const $MEDIA = $COND + ? await loadWebMedia($SOURCE, { + ..., + sandboxValidated: true, + ..., + }) + : await loadWebMedia($SOURCE, { + ..., + localRoots: $ROOTS, + ..., + }); + - pattern-not-inside: | + if ($COND) { + ... + await loadWebMedia($SOURCE, { + ..., + sandboxValidated: true, + ..., + }); + ... + } else { + ... + await loadWebMedia($SOURCE, { + ..., + localRoots: $ROOTS, + ..., + }); + ... + } + metadata: + ghsa: GHSA-FQCM-97M6-W7RM + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-FQCM-97M6-W7RM + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-unconditional-sandboxvalidated-loadwebmedia + - id: ghsa-fqrj-m88p-qf3v.openclaw-zalo-replay-dedupe-key-missing-target-scope + languages: + - typescript + - javascript + severity: INFO + message: Zalo replay dedupe key is built only from event_name and messageId. Replay caches for multi-account webhook targets must include account/path target scope. See GHSA-FQRJ-M88P-QF3V. + metadata: + ghsa: GHSA-FQRJ-M88P-QF3V + category: security + cwe: + - CWE-667 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-FQRJ-M88P-QF3V + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-zalo-replay-dedupe-key-missing-target-scope + paths: + include: + - extensions/zalo/src/**/*.ts + pattern-either: + - pattern: const $KEY = `${$UPDATE.event_name}:${messageId}` + - pattern: const $KEY = `${$UPDATE.event_name}:${$MESSAGE_ID}` + - id: ghsa-fv94-qvg8-xqpw.ssh-sandbox-upload-missing-symlink-boundary-check + languages: + - typescript + - javascript + severity: WARNING + message: SSH sandbox upload creates a tar stream from a local directory without a preceding symlink boundary validation helper, so tar may follow escaping symlinks before upload. + patterns: + - pattern-inside: | + async function $FUNC(...){ + ... + } + - pattern: | + $TAR = spawn("tar", ["-C", $LOCAL, "-cf", "-", "."], ...) + - pattern: | + $SSH = spawn($CMD, $ARGS, ...) + - pattern-not-inside: | + async function $FUNC(...){ + ... + await $CHECK($LOCAL); + ... + $TAR = spawn("tar", ["-C", $LOCAL, "-cf", "-", "."], ...) + ... + } + metadata: + category: security + confidence: medium + ghsa: GHSA-FV94-QVG8-XQPW + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-FV94-QVG8-XQPW + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: ssh-sandbox-upload-missing-symlink-boundary-check + - id: ghsa-g353-mgv3-8pcj.feishu-webhook-mode-missing-encrypt-key + message: Feishu/Lark webhook-mode configuration sets verificationToken without also configuring encryptKey. + severity: ERROR + languages: + - typescript + - javascript + metadata: + category: security + technology: + - feishu + - lark + confidence: low + note: Review aid only; misses inherited/defaulted config and may not prove runtime exploitability. + ghsa: GHSA-G353-MGV3-8PCJ + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-G353-MGV3-8PCJ + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: feishu-webhook-mode-missing-encrypt-key + patterns: + - pattern-either: + - pattern: | + { + ..., + connectionMode: "webhook", + ..., + verificationToken: $TOKEN, + ... + } + - pattern: | + $CFG = { + ..., + connectionMode: "webhook", + ..., + verificationToken: $TOKEN, + ... + } + - pattern-not: | + { + ..., + encryptKey: $KEY, + ... + } + - pattern-not: | + $CFG = { + ..., + encryptKey: $KEY, + ... + } + - id: ghsa-g86v-f9qv-rh6m.openclaw-missing-discard-ipv6-special-use-range + languages: + - typescript + - javascript + severity: WARNING + message: IPv6 special-use blocking set is typed from ipaddr IPv6Range without the runtime "discard" range, so isBlockedSpecialUseIpv6Address can miss 100::/64 discard addresses. + patterns: + - pattern-either: + - patterns: + - pattern: | + const BLOCKED_IPV6_SPECIAL_USE_RANGES = new Set([ + ... + ]); + - pattern-not: | + const BLOCKED_IPV6_SPECIAL_USE_RANGES = new Set([ + ..., + "discard", + ... + ]); + - pattern-not-inside: | + type $T = $R | "discard"; + ... + - pattern-not-inside: | + type $T = "discard" | $R; + ... + - patterns: + - pattern: | + const BLOCKED_IPV6_SPECIAL_USE_RANGES = new Set>([ + ... + ]); + - pattern-not: | + const BLOCKED_IPV6_SPECIAL_USE_RANGES = new Set>([ + ..., + "discard", + ... + ]); + - patterns: + - pattern: | + const BLOCKED_IPV6_SPECIAL_USE_RANGES = new Set>([ + ... + ]); + - pattern-not: | + const BLOCKED_IPV6_SPECIAL_USE_RANGES = new Set>([ + ..., + "discard", + ... + ]); + metadata: + ghsa: GHSA-G86V-F9QV-RH6M + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-G86V-F9QV-RH6M + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-missing-discard-ipv6-special-use-range + - id: ghsa-g8p2-7wf7-98mq.query-gatewayurl-applied-without-confirmation + languages: + - typescript + - javascript + severity: ERROR + message: Untrusted gatewayUrl from URL params/hash is written directly into live gateway settings. Route the value through a pending confirmation flow before reconnecting or applying tokens. + patterns: + - pattern-either: + - patterns: + - pattern: | + $RAW = $PARAMS.get("gatewayUrl"); + ... + if ($RAW != null) { + ... + applySettings($HOST, { ...$HOST.settings, gatewayUrl: $URL, ... }); + ... + } + - pattern-not: | + $HOST.pendingGatewayUrl = ... + - patterns: + - pattern-either: + - pattern: $URL = $PARAMS.get("gatewayUrl") + - pattern: $URL = ($PARAMS.get("gatewayUrl") ?? "").trim() + - pattern-inside: | + if ($COND) { + ... + $HOST.settings = { ...$HOST.settings, gatewayUrl: $NEXT, ... }; + ... + } + - pattern-not-inside: | + $HOST.pendingGatewayUrl = ... + metadata: + ghsa: GHSA-G8P2-7WF7-98MQ + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-G8P2-7WF7-98MQ + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: query-gatewayurl-applied-without-confirmation + - id: ghsa-g99v-8hwm-g76g.openclaw-web-search-citation-redirect-private-network-policy + languages: + - typescript + - javascript + message: Citation redirect resolution should not use a private-network-allowing SSRF policy for HEAD redirect expansion. + severity: ERROR + patterns: + - pattern-either: + - pattern: | + withWebToolsNetworkGuard({ + ..., + init: { method: "HEAD", ... }, + ..., + policy: { dangerouslyAllowPrivateNetwork: true, ... }, + ... + }, ...) + - pattern: | + fetchWithWebToolsNetworkGuard({ + ..., + init: { method: "HEAD", ... }, + ..., + policy: { dangerouslyAllowPrivateNetwork: true, ... }, + ... + }) + - pattern: | + withWebToolsNetworkGuard({ + ..., + init: { method: "HEAD", ... }, + ..., + policy: $POLICY, + ... + }, ...) + - pattern: | + fetchWithWebToolsNetworkGuard({ + ..., + init: { method: "HEAD", ... }, + ..., + policy: $POLICY, + ... + }) + - metavariable-pattern: + metavariable: $POLICY + patterns: + - pattern-either: + - pattern: WEB_TOOLS_TRUSTED_NETWORK_SSRF_POLICY + - pattern: TRUSTED_POLICY + metadata: + category: security + cwe: CWE-918 + ghsa: GHSA-G99V-8HWM-G76G + rationale: HEAD-based citation redirect resolution must not opt into private-network SSRF policy. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-G99V-8HWM-G76G + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-web-search-citation-redirect-private-network-policy + - id: ghsa-gcj7-r3hg-m7w6.replay-key-derived-from-idempotency-header + message: Replay or dedupe key derived from an idempotency/replay header can let unsigned metadata control request identity. + severity: WARNING + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + const $TOKEN = $GET(..., $HEADER, ...); + ... + if ($TOKEN) { + ... + return `...${$TOKEN}...`; + } + - pattern: | + const $TOKEN = $GET(..., $HEADER); + ... + if ($TOKEN) { + ... + return `...${$TOKEN}...`; + } + - pattern: | + const $TOKEN = $HEADERS[$HEADER]; + ... + if ($TOKEN) { + ... + return `...${$TOKEN}...`; + } + - metavariable-regex: + metavariable: $HEADER + regex: (?i).*(idempotency|replay|dedupe).* + metadata: + category: security + cwe: + - CWE-294 + - CWE-345 + ghsa: GHSA-GCJ7-R3HG-M7W6 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-GCJ7-R3HG-M7W6 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: replay-key-derived-from-idempotency-header + - id: ghsa-gfmx-pph7-g46x.openclaw.system-event-missing-explicit-trust + languages: + - typescript + - javascript + severity: ERROR + message: | + enqueueSystemEvent() is called with interpolated or variable text without `trusted: false`. The default is `trusted: true`, which injects the text as a privileged `System:` prefix in the agent's context window. External content — channel messages, user IDs, event payloads, exec output — MUST be explicitly downgraded with `trusted: false` to prevent prompt injection. See GHSA-GFMX-PPH7-G46X. + TRIAGE NOTE: If ALL interpolated values in the template literal are boolean flags or enum/const expressions (e.g. `${x ? "on" : "off"}`), or if the variable text is formatted from fully-internal state (not external channel content), the finding may be low-risk. Add `trusted: true` explicitly to self-document that the text is intentionally trusted. + metadata: + category: security + cwe: + - CWE-74 + - CWE-77 + ghsas: + - GHSA-GFMX-PPH7-G46X + ghsa: GHSA-GFMX-PPH7-G46X + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-GFMX-PPH7-G46X + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.system-event-missing-explicit-trust + paths: + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - src/auto-reply/reply/directive-handling.impl.ts + - src/auto-reply/reply/directive-handling.persist.ts + - src/auto-reply/reply/get-reply-directives-apply.ts + - src/gateway/config-recovery-notice.ts + - src/gateway/server-restart-sentinel.ts + - src/infra/session-maintenance-warning.ts + pattern-either: + - patterns: + - pattern: | + enqueueSystemEvent(`...${$X}...`, $OPTS) + - pattern-not: | + enqueueSystemEvent(`...${$X}...`, { ..., trusted: $V, ... }) + - pattern-not-inside: | + enqueueSystemEvent(`...${$X}...`, { ..., trusted: $V, ... }) + - patterns: + - pattern: | + enqueueSystemEvent($TEXT, $OPTS) + - pattern-not: | + enqueueSystemEvent($TEXT, { ..., trusted: $V, ... }) + - pattern-not-inside: | + enqueueSystemEvent($TEXT, { ..., trusted: $V, ... }) + - metavariable-regex: + metavariable: $TEXT + regex: ^[a-zA-Z_$][a-zA-Z0-9_$]*$ + - id: ghsa-gg9v-mgcp-v6m7.openclaw-bootstrap-token-legacy-acceptance-without-profile-record + languages: + - typescript + severity: ERROR + message: verifyDeviceBootstrapToken() consumes a bootstrap token record without resolving the persisted bootstrap profile. Legacy acceptance does not bind setup codes to issued role/scopes and can allow first-use privilege escalation. See GHSA-GG9V-MGCP-V6M7. + metadata: + ghsa: GHSA-GG9V-MGCP-V6M7 + category: security + cwe: + - CWE-269 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-GG9V-MGCP-V6M7 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-bootstrap-token-legacy-acceptance-without-profile-record + paths: + include: + - src/infra/device-bootstrap.ts + pattern-regex: const \[tokenKey\] = found; + - id: ghsa-gq9c-wg68-gwj2.browser-writable-output-path-bypasses-resolvewritablepathwithinroot + languages: + - typescript + severity: ERROR + message: Browser route is using resolvePathWithinRoot for a writable output path. For writable destinations (downloads, traces) use resolveWritablePathWithinRoot which performs symlink-aware canonical parent checks. The lexical-only resolvePathWithinRoot can be defeated by a symlink at the parent dir. See GHSA-GQ9C-WG68-GWJ2. + metadata: + ghsa: GHSA-GQ9C-WG68-GWJ2 + category: security + cwe: + - CWE-22 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-GQ9C-WG68-GWJ2 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: browser-writable-output-path-bypasses-resolveWritablePathWithinRoot + paths: + include: + - extensions/browser/src/browser/routes/**.ts + - src/browser/routes/**.ts + exclude: + - "**/paths.ts" + - "**/path-output.ts" + pattern: resolvePathWithinRoot(...) + - id: ghsa-h7f7-89mm-pqh6.openclaw-skill-download-targetdir-outside-tools-root + languages: + - typescript + - javascript + message: Untrusted skill download targetDir resolves outside the per-skill tools root. + severity: ERROR + patterns: + - pattern-either: + - pattern: | + function $F(..., $SPEC, ...) { + ... + if ($SPEC.targetDir?.trim()) { + return resolveUserPath($SPEC.targetDir); + } + ... + } + - pattern: | + const $TARGET = $SPEC.targetDir?.trim() ? resolveUserPath($SPEC.targetDir) : $FALLBACK; + - pattern-not-inside: | + if (!isWithinDir(...)) { + ... + } + - pattern-not-inside: | + await assertCanonicalPathWithinBase({ + ... + }) + metadata: + ghsa: GHSA-H7F7-89MM-PQH6 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-H7F7-89MM-PQH6 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-skill-download-targetdir-outside-tools-root + - id: ghsa-h9xm-j4qg-fvpg.apply-patch-sandbox-hostpath-without-workspace-guard + languages: + - typescript + severity: ERROR + message: Sandbox apply_patch path resolution should not return sandbox bridge host paths without re-applying assertSandboxPath when workspaceOnly remains enabled. + patterns: + - pattern-inside: | + async function resolvePatchPath(...) { + ... + } + - pattern: | + if (options.sandbox) { + const resolved = options.sandbox.bridge.resolvePath({ + ... + }); + ... + return { + resolved: resolved.hostPath, + ... + }; + } + - pattern-not: | + if (options.sandbox) { + const resolved = options.sandbox.bridge.resolvePath({ + ... + }); + await assertSandboxPath({ + filePath: resolved.hostPath, + cwd: options.cwd, + root: options.cwd, + ... + }); + return { + resolved: resolved.hostPath, + ... + }; + } + metadata: + ghsa: GHSA-H9XM-J4QG-FVPG + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-H9XM-J4QG-FVPG + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: apply-patch-sandbox-hostpath-without-workspace-guard + - id: ghsa-j26j-7qc4-3mrf.teams-file-consent-missing-conversation-binding + languages: + - typescript + - javascript + severity: WARNING + message: fileConsent/invoke handlers should bind pending uploads to the invoking conversation before consuming upload state; uploadId-only pending-upload lookup can allow cross-conversation use. + patterns: + - pattern-inside: | + if ($ACTIVITY.type !== "invoke" || $ACTIVITY.name !== "fileConsent/invoke") { + ... + } + ... + - pattern: | + const $PENDING = getPendingUpload($UPLOAD_ID); + ... + if ($ACTION === "accept" && $UPLOAD_INFO) { + if ($PENDING) { + ... + uploadToConsentUrl({ + ... + }); + ... + } + } else { + ... + removePendingUpload($UPLOAD_ID); + } + - pattern-not-inside: | + if ($PENDING) { + ... + if (!$INVOKE_CONV || $PENDING_CONV !== $INVOKE_CONV) { + ... + } + } + metadata: + confidence: medium + detector_review: family_candidate + rationale: Matches Teams fileConsent invoke handlers that both upload and remove pending uploads based on uploadId-controlled lookup without a same-handler conversation-binding guard. + ghsa: GHSA-J26J-7QC4-3MRF + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-J26J-7QC4-3MRF + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: teams-file-consent-missing-conversation-binding + - id: ghsa-j425-whc4-4jgc.openclaw-dangerous-host-env-override-pivots + message: Dangerous host env override pivot reaches host env sanitization boundary or policy. + severity: ERROR + languages: + - json + - typescript + - javascript + pattern-either: + - patterns: + - pattern-inside: | + { ... } + - pattern-regex: '"(GIT_SSH_COMMAND|GIT_EXEC_PATH|GIT_TEMPLATE_DIR|YARN_RC_FILENAME|CC|CXX|CMAKE_C_COMPILER|CMAKE_CXX_COMPILER|RUSTC_WRAPPER|CARGO_BUILD_RUSTC|CARGO_BUILD_RUSTC_WRAPPER|MAKEFLAGS|MFLAGS)"' + - patterns: + - pattern-inside: | + { ... } + - pattern-regex: '"(GIT_CONFIG_[A-Z0-9_]+|NPM_CONFIG_[A-Z0-9_]+|CARGO_REGISTRIES_[A-Z0-9_]+|TF_VAR_[A-Z0-9_]+)"' + - patterns: + - pattern-either: + - pattern: | + sanitizeHostExecEnv({ ..., overrides: { ..., $KEY: $VALUE, ... }, ... }) + - pattern: | + sanitizeSystemRunEnvOverrides({ ..., overrides: { ..., $KEY: $VALUE, ... }, ... }) + - metavariable-regex: + metavariable: $KEY + regex: ^(GIT_SSH_COMMAND|GIT_EXEC_PATH|GIT_TEMPLATE_DIR|YARN_RC_FILENAME|CC|CXX|CMAKE_C_COMPILER|CMAKE_CXX_COMPILER|RUSTC_WRAPPER|CARGO_BUILD_RUSTC|CARGO_BUILD_RUSTC_WRAPPER|MAKEFLAGS|MFLAGS|GIT_CONFIG_[A-Z0-9_]+|NPM_CONFIG_[A-Z0-9_]+|CARGO_REGISTRIES_[A-Z0-9_]+|TF_VAR_[A-Z0-9_]+)$ + metadata: + ghsa: GHSA-J425-WHC4-4JGC + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-J425-WHC4-4JGC + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-dangerous-host-env-override-pivots + - id: ghsa-jf25-7968-h2h5.openclaw-nodes-screen-record-outpath-guard-missing + message: The nodes tool (which contains screen_record) must be wrapped via applyNodesToolWorkspaceGuard, which adds outPath to the pathParamKeys. Without this, screen_record outPath bypasses the workspace boundary. See GHSA-JF25-7968-H2H5. + severity: ERROR + languages: + - typescript + metadata: + ghsa: GHSA-JF25-7968-H2H5 + category: security + cwe: + - CWE-22 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-JF25-7968-H2H5 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-nodes-screen-record-outpath-guard-missing + paths: + include: + - src/agents/openclaw-tools.ts + patterns: + - pattern: createNodesTool(...) + - pattern-not-inside: | + import { ..., applyNodesToolWorkspaceGuard, ... } from "$X"; + ... + - pattern-not-inside: | + import { applyNodesToolWorkspaceGuard } from "$X"; + ... + - pattern-not-inside: | + import { applyNodesToolWorkspaceGuard, ... } from "$X"; + ... + - id: ghsa-jfv4-h8mc-jcp8.immediate-process-tree-sigkill + message: Immediate process-tree SIGKILL inside a termination helper deserves review for missing graceful shutdown and ownership checks. + severity: WARNING + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: process.kill(-$PID, "SIGKILL") + - pattern: process.kill(-$PID, 9) + - pattern-inside: | + function $F(...) { + ... + } + - metavariable-regex: + metavariable: $F + regex: (?i).*(kill|terminate|stop|destroy).* + - pattern-not-inside: | + function $F(...) { + ... + process.kill(-$PID, "SIGTERM") + ... + process.kill(-$PID, "SIGKILL") + ... + } + - pattern-not-inside: | + function $F(...) { + ... + process.kill(-$PID, 0) + ... + process.kill(-$PID, "SIGKILL") + ... + } + metadata: + ghsa: GHSA-JFV4-H8MC-JCP8 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-JFV4-H8MC-JCP8 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: immediate-process-tree-sigkill + - id: ghsa-jj6q-rrrf-h66h.timing-safe-equal-length-short-circuit + message: Secret comparison short-circuits on length before timingSafeEqual; hash to a fixed length first or use a fixed-length helper. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + if ($A.length !== $B.length) { + ... + return false; + } + ... + return $CRYPTO.timingSafeEqual($A, $B); + - pattern: | + if ($A.length !== $B.length) return false; + ... + return $CRYPTO.timingSafeEqual($A, $B); + - pattern-not: | + return $CRYPTO.timingSafeEqual($HASH(...), $HASH2(...)); + metadata: + category: security + confidence: medium + ghsa: GHSA-JJ6Q-RRRF-H66H + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-JJ6Q-RRRF-H66H + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: timing-safe-equal-length-short-circuit + - id: ghsa-jjw7-3vjf-fg5j.openclaw-config-private-key-string-field + languages: + - typescript + - javascript + message: Config schemas that model a privateKey field as a plain string can bypass secret redaction. Prefer buildSecretInputSchema()/SecretInputSchema and sensitive registration. + severity: ERROR + patterns: + - pattern-either: + - pattern: | + privateKey: z.string().optional() + - pattern: | + privateKey: z.string() + - pattern-inside: | + z.object({ + ... + }) + metadata: + ghsa: GHSA-JJW7-3VJF-FG5J + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-JJW7-3VJF-FG5J + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-config-private-key-string-field + - id: ghsa-jmm5-fvh5-gf4p.ts-early-length-check-before-timingsafeequal + message: Early length mismatch return before timingSafeEqual can leak secret length; hash or pad to fixed-length before comparing. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-inside: | + function $F(...){ + ... + } + - pattern-either: + - pattern: | + const $A = Buffer.from($LEFT, ...); + const $B = Buffer.from($RIGHT, ...); + if ($A.length !== $B.length) { + return false; + } + ... + return timingSafeEqual($A, $B); + - pattern: | + const $A = Buffer.from($LEFT, ...); + const $B = Buffer.from($RIGHT, ...); + if ($A.length !== $B.length) return false; + ... + return timingSafeEqual($A, $B); + metadata: + category: security + cwe: + - CWE-208 + ghsa: GHSA-JMM5-FVH5-GF4P + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-JMM5-FVH5-GF4P + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: ts-early-length-check-before-timingsafeequal + - id: ghsa-jp4j-q5fc-58gv.discord-component-missing-group-policy + languages: + - typescript + message: Discord component ingress calls ensureGuildComponentMemberAllowed without groupPolicy, which skips guild/channel group policy enforcement. + severity: ERROR + patterns: + - pattern: | + await ensureGuildComponentMemberAllowed({ + ... + }) + - pattern-not: | + await ensureGuildComponentMemberAllowed({ + ..., + groupPolicy: $POLICY, + ... + }) + metadata: + ghsa: GHSA-JP4J-Q5FC-58GV + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-JP4J-Q5FC-58GV + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: discord-component-missing-group-policy + - id: ghsa-jq3f-vjww-8rq7.webhook-body-read-before-secret-validation + languages: + - typescript + - javascript + severity: ERROR + message: Webhook handlers that read a request body before extracting the Telegram secret header should be reviewed for pre-auth resource exhaustion. + patterns: + - pattern-inside: | + createServer(($REQ, $RES) => { + ... + }) + - pattern: | + const $BODY = await readJsonBodyWithLimit($REQ, ...); + ... + const $RAW = $REQ.headers["x-telegram-bot-api-secret-token"]; + - pattern-not: | + const $RAW = $REQ.headers["x-telegram-bot-api-secret-token"]; + ... + if (!hasValidTelegramWebhookSecret(...)) { + ... + return; + } + ... + const $BODY = await readJsonBodyWithLimit($REQ, ...); + metadata: + ghsa: GHSA-JQ3F-VJWW-8RQ7 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-JQ3F-VJWW-8RQ7 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: webhook-body-read-before-secret-validation + - id: ghsa-jqpq-mgvm-f9r6.openclaw-project-local-node-modules-bin-prepended-to-path + languages: + - typescript + severity: ERROR + message: Project-local node_modules/.bin is included in the PATH prepend candidate list. This can allow a malicious workspace to hijack allowlisted command resolution. Project-local bin must be opt-in and append-only. See GHSA-JQPQ-MGVM-F9R6. + metadata: + ghsa: GHSA-JQPQ-MGVM-F9R6 + category: security + cwe: + - CWE-426 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-JQPQ-MGVM-F9R6 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-project-local-node-modules-bin-prepended-to-path + paths: + include: + - src/infra/path-env.ts + pattern-either: + - pattern: candidates.push(localBinDir) + - pattern: prepend.push(localBinDir) + - id: ghsa-m34q-h93w-vg5x.openshell-unsafe-remote-path-normalization + languages: + - typescript + severity: ERROR + message: OpenShell remote path configs normalized without managed-root enforcement can escape /sandbox or /agent. + patterns: + - pattern-either: + - pattern: | + remoteWorkspaceDir: $FN(...), + - pattern: | + remoteAgentWorkspaceDir: $FN(...), + - metavariable-regex: + metavariable: $FN + regex: ^(?:normalizeRemotePath|cleanRemotePath|path\.posix\.normalize)$ + metadata: + category: security + ghsa: GHSA-M34Q-H93W-VG5X + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-M34Q-H93W-VG5X + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openshell-unsafe-remote-path-normalization + - id: ghsa-m3mh-3mpg-37hw.openclaw-staged-package-install-without-hidden-project-npmrc + languages: + - typescript + message: Running npm install in a copied package directory without first hiding the staged project .npmrc can let attacker-controlled npm config influence install-time tool execution. + severity: WARNING + patterns: + - pattern: | + $RES = await runCommandWithTimeout( + ["npm", "install", "--omit=dev", "--silent", "--ignore-scripts"], + {..., cwd: $STAGE, ...}, + ) + - pattern-not-inside: | + $HIDDEN = await hideProjectNpmConfigForInstall($STAGE); + ... + try { + ... + $RES = await runCommandWithTimeout( + ["npm", "install", "--omit=dev", "--silent", "--ignore-scripts"], + {..., cwd: $STAGE, ...}, + ) + ... + } finally { + await restoreProjectNpmConfigAfterInstall($HIDDEN); + } + - pattern-not-inside: | + $HIDDEN = await hideProjectNpmConfigForInstall($STAGE); + ... + try { + ... + await runCommandWithTimeout( + ["npm", "install", "--omit=dev", "--silent", "--ignore-scripts"], + {..., cwd: $STAGE, ...}, + ) + ... + } finally { + await restoreProjectNpmConfigAfterInstall($HIDDEN); + } + metadata: + ghsa: GHSA-M3MH-3MPG-37HW + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-M3MH-3MPG-37HW + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-staged-package-install-without-hidden-project-npmrc + - id: ghsa-m69h-jm2f-2pv8.feishu-reaction-chat-type-defaults-to-p2p + languages: + - typescript + severity: WARNING + message: Synthetic Feishu reaction events must preserve verified chat type instead of defaulting non-group reactions to p2p. + patterns: + - pattern-either: + - pattern: | + const $TYPE: $CHAT = $EVENT.chat_type === "group" ? "group" : "p2p"; + - pattern: | + const $TYPE = $EVENT.chat_type === "group" ? "group" : "p2p"; + metadata: + ghsa: GHSA-M69H-JM2F-2PV8 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-M69H-JM2F-2PV8 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: feishu-reaction-chat-type-defaults-to-p2p + - id: ghsa-m7x8-2w3w-pr42.ghsa-m7x8-shell-interpolated-gh-api-login + message: Unvalidated login-like input interpolated into an execSync shell command that invokes gh api users/... + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + execSync(`gh api users/${$LOGIN}`, ...) + - pattern: | + execSync(`gh api users/${$LOGIN}...`, ...) + - pattern: | + execSync("gh api users/" + $LOGIN, ...) + - pattern: | + execSync("gh api users/" + $LOGIN + ..., ...) + - pattern: | + execSync('gh api users/' + $LOGIN, ...) + - pattern: | + execSync('gh api users/' + $LOGIN + ..., ...) + metadata: + ghsa: GHSA-M7X8-2W3W-PR42 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-M7X8-2W3W-PR42 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: ghsa-m7x8-shell-interpolated-gh-api-login + - id: ghsa-mc68-q9jw-2h3v.openclaw-docker-path-env-shell-interpolation + languages: + - typescript + - javascript + severity: ERROR + message: Caller-controlled params.env.PATH is interpolated directly into an `export PATH=...` shell snippet. Pass it via an environment variable and reference the variable from constant shell text instead. See GHSA-MC68-Q9JW-2H3V. + metadata: + ghsa: GHSA-MC68-Q9JW-2H3V + category: security + cwe: + - CWE-78 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-MC68-Q9JW-2H3V + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-docker-path-env-shell-interpolation + paths: + include: + - src/agents/bash-tools.shared.ts + pattern-regex: "`[^`]*export\\s+PATH=[^`]*\\$\\{params\\.env\\.PATH\\}[^`]*`" + - id: ghsa-mf5g-6r6f-ghhm.webhook-invalid-token-without-preauth-lockout + message: Webhook invalid-token branches should enforce a pre-auth lockout or failure budget before returning 401. + severity: WARNING + languages: + - typescript + patterns: + - pattern-inside: | + function $FUNC(...){ + ... + } + - pattern: | + if (!validateToken($TOKEN, $EXPECTED)) { + ... + return { ok: false, statusCode: 401, error: "Invalid token" }; + } + - pattern-not-inside: | + if ($RATE_LIMITER.isLocked(...)) { + ... + return { ok: false, statusCode: 429, error: "Rate limit exceeded" }; + } + - pattern-not-inside: | + if (!validateToken($TOKEN, $EXPECTED)) { + ... + if ($RATE_LIMITER.recordFailure(...)) { + ... + return { ok: false, statusCode: 429, error: "Rate limit exceeded" }; + } + ... + return { ok: false, statusCode: 401, error: "Invalid token" }; + } + metadata: + category: security + ghsa: GHSA-MF5G-6R6F-GHHM + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-MF5G-6R6F-GHHM + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: webhook-invalid-token-without-preauth-lockout + - id: ghsa-mmpf-jwf4-h3qv.shell-git-add-paths-must-use-double-dash + languages: + - bash + severity: ERROR + message: Passing filenames into git add without a `--` separator can let option-like paths be interpreted as flags. Use `git add -- ...` and avoid piping path text through xargs. + pattern-either: + - patterns: + - pattern: echo "$FILES" | xargs git add + - patterns: + - pattern: printf ... | xargs git add + - patterns: + - pattern: ... | xargs git add + - metavariable-regex: + metavariable: $... + regex: .* + - patterns: + - pattern: git add $FILES + - patterns: + - pattern: git add "$FILES" + - patterns: + - pattern: git add ${FILES[@]} + - patterns: + - pattern: git add "${FILES[@]}" + - patterns: + - pattern: git add ${files[@]} + - patterns: + - pattern: git add "${files[@]}" + pattern-not-either: + - pattern: git add -- $FILES + - pattern: git add -- "$FILES" + - pattern: git add -- ${FILES[@]} + - pattern: git add -- "${FILES[@]}" + - pattern: git add -- ${files[@]} + - pattern: git add -- "${files[@]}" + metadata: + ghsa: GHSA-MMPF-JWF4-H3QV + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-MMPF-JWF4-H3QV + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: shell-git-add-paths-must-use-double-dash + - id: ghsa-mp66-rf4f-mhh8.googlechat-app-url-addon-principal-missing-binding + message: Google Chat app-url webhook auth accepts add-on principals without comparing the token principal to a configured binding. + severity: ERROR + languages: + - typescript + - javascript + metadata: + ghsa: GHSA-MP66-RF4F-MHH8 + detector: A + category: security + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-MP66-RF4F-MHH8 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: googlechat-app-url-addon-principal-missing-binding + patterns: + - pattern: | + const $OK = + $PAYLOAD?.email_verified && + ($EMAIL === $CHAT_ISSUER || $ADDON_ISSUER_PATTERN.test($EMAIL)); + - pattern-not: | + const $EXPECTED = $PARAMS.expectedAddOnPrincipal?.trim().toLowerCase(); + - pattern-not: | + const $EXPECTED = $INPUT.expectedAddOnPrincipal?.trim().toLowerCase(); + - pattern-not: | + const $EXPECTED = $CFG.expectedAddOnPrincipal?.trim().toLowerCase(); + paths: + include: + - "**/*.ts" + - "**/*.js" + - id: ghsa-mwxv-35wr-4vvj.openclaw.gateway.api-channels-prefix-auth-gap + languages: + - typescript + message: Gateway plugin routes that protect only /api/channels/* but not the exact /api/channels root can leave the channel namespace auth-bypassable. + severity: ERROR + patterns: + - pattern-inside: | + if (handlePluginRequest) { + ... + } + - pattern: | + if ($REQ_PATH.startsWith("/api/channels/")) { + ... + } + - pattern-not: | + if ($REQ_PATH === "/api/channels" || $REQ_PATH.startsWith("/api/channels/")) { + ... + } + metadata: + ghsa: GHSA-MWXV-35WR-4VVJ + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-MWXV-35WR-4VVJ + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.gateway.api-channels-prefix-auth-gap + - id: ghsa-p4x4-2r7f-wjxg.openclaw-wrapper-carrier-allow-always + message: Positional shell carrier persists a carried executable without first rejecting shell or dispatch wrappers. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + const $RESOLUTION = resolveCommandResolutionFromArgv([$CARRIED], $CWD, $ENV); + ... + return resolveExecutionTargetCandidatePath($RESOLUTION, $CWD); + - pattern: | + const $RESOLUTION = resolveCommandResolutionFromArgv([$CARRIED], $DIR, $ENVIRONMENT); + ... + return resolveExecutionTargetCandidatePath($RESOLUTION, $DIR); + - pattern-not-inside: | + const $NAME = normalizeExecutableToken($CARRIED); + ... + if (isDispatchWrapperExecutable($NAME) || isShellWrapperExecutable($NAME)) { + return undefined; + } + ... + metadata: + ghsa: GHSA-P4X4-2R7F-WJXG + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-P4X4-2R7F-WJXG + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-wrapper-carrier-allow-always + - id: ghsa-p536-vvpp-9mc8.openclaw.web-fetch-or-media-unbounded-response-read + languages: + - typescript + - javascript + severity: WARNING + message: | + Inside the openclaw web-fetch / media / SSRF-guarded fetch family, calling .text() / .json() / .arrayBuffer() / .blob() etc. directly on a Response that may have come from an untrusted (model- or plugin-controlled) URL can OOM the process if the upstream returns a multi-GB body. Use readResponseWithLimit() / readResponseTextSnippet() (which stream-and-bound) instead. A pre-buffer Content-Length check is not sufficient — the upstream can lie or omit Content-Length. + metadata: + category: security + cwe: + - CWE-400 + ghsas: + - GHSA-P536-VVPP-9MC8 + - GHSA-J27P-HQ53-9WGC + ghsa: GHSA-P536-VVPP-9MC8 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-P536-VVPP-9MC8 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.web-fetch-or-media-unbounded-response-read + paths: + include: + - src/web-fetch/** + - src/agents/tools/web-fetch*.ts + - src/agents/tools/**/web-fetch*.ts + - src/plugins/web-fetch-providers*.ts + - src/media/** + - src/infra/net/** + exclude: + - src/media/read-response-with-limit.ts + - src/media/read-response-with-limit.test.ts + patterns: + - pattern-either: + - pattern: $RESP.text() + - pattern: $RESP.json() + - pattern: $RESP.arrayBuffer() + - pattern: $RESP.blob() + - pattern: $RESP.bytes() + - pattern: $RESP.formData() + - pattern-not-inside: | + readResponseTextSnippet($RESP, ...) + - pattern-not-inside: | + readResponseWithLimit($RESP, ...) + - id: ghsa-pfv7-rr5m-qmv6.local-relay-json-endpoint-missing-auth-header-check + languages: + - typescript + - javascript + severity: WARNING + message: Local relay servers that expose privileged /json* endpoints should enforce a relay-auth header before serving them. + patterns: + - pattern-inside: | + createServer(($REQ, $RES) => { + ... + }) + - pattern-either: + - pattern: | + if (($PATH === "/json/version" || $PATH === "/json/version/") && $COND) { + ... + } + - pattern: | + if ($LIST.has($PATH) && $COND) { + ... + } + - pattern: | + if ($PATH.startsWith("/json")) { + ... + } + - pattern-not-inside: | + if ($PATH.startsWith("/json")) { + ... + const $TOKEN = ...; + if (!$TOKEN || $TOKEN !== $AUTH) { + ... + return; + } + ... + } + metadata: + ghsa: GHSA-PFV7-RR5M-QMV6 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-PFV7-RR5M-QMV6 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: local-relay-json-endpoint-missing-auth-header-check + - id: ghsa-q2qc-744p-66r2.ghsa-q2qc-post-resolution-agent-prefix-guard + languages: + - typescript + - javascript + message: Authorization/visibility logic records whether the original session reference was an explicit agent key, then rewrites that reference to a resolved canonical key and later re-checks the rewritten value with startsWith("agent:") instead of using the original explicit-key flag. + severity: ERROR + patterns: + - pattern-inside: | + const $EXPLICIT = $RAW.startsWith("agent:"); + ... + $RAW = $RESOLVED; + ... + if ($GUARD && !$RAW.startsWith("agent:")) { + ... + } + - metavariable-pattern: + metavariable: $RESOLVED + pattern-either: + - pattern: $OBJ.key + - pattern: $CALL(...) + - pattern-not-inside: | + if ($GUARD && !$EXPLICIT) { + ... + } + metadata: + ghsa: GHSA-Q2QC-744P-66R2 + category: authz-bypass + confidence: medium + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-Q2QC-744P-66R2 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: ghsa-q2qc-post-resolution-agent-prefix-guard + - id: ghsa-q447-rj3r-2cgh.openclaw.webhook-request-stream-read-without-shared-limit-helper + languages: + - typescript + severity: ERROR + message: Webhook/request handler manually reads request stream data instead of using the shared bounded body helpers. Manual req.on("data") readers often miss unified maxBytes + timeoutMs enforcement and can reintroduce oversized-body or slow-upload DoS. See GHSA-Q447-RJ3R-2CGH. + metadata: + ghsa: GHSA-Q447-RJ3R-2CGH + category: security + cwe: + - CWE-400 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-Q447-RJ3R-2CGH + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.webhook-request-stream-read-without-shared-limit-helper + paths: + include: + - extensions/**/src/**/monitor*.ts + - extensions/**/src/**/webhook*.ts + - extensions/**/src/**/nostr-profile-http.ts + - src/gateway/hooks.ts + - src/line/**/*.ts + - src/slack/**/*.ts + - src/telegram/**/*.ts + exclude: + - "**/*.test.ts" + - src/infra/http-body.ts + - src/plugin-sdk/** + patterns: + - pattern-either: + - pattern: $REQ.on("data", ...) + - pattern: $REQ.on('data', ...) + - metavariable-regex: + metavariable: $REQ + regex: ^(req|request|params\.req)$ + - pattern-not-inside: | + readRequestBodyWithLimit(...) + - pattern-not-inside: | + readJsonBodyWithLimit(...) + - pattern-not-inside: | + installRequestBodyLimitGuard(...) + - id: ghsa-q6qf-4p5j-r25g.openclaw.sandbox-image-tool.workspace-only-omission + languages: + - typescript + severity: WARNING + message: Sandboxed image tool path resolution is using the legacy helper that never enforces tools.fs.workspaceOnly for mounted host paths. This shape is the vulnerable pre-fix image-tool family from GHSA-Q6QF-4P5J-R25G. + pattern: | + await resolveSandboxedImagePath({ + sandbox: $SANDBOX, + imagePath: $IMAGE, + }) + metadata: + cwe: + - CWE-200 + - CWE-284 + ghsa: GHSA-Q6QF-4P5J-R25G + category: security + confidence: medium + rationale: The vulnerable helper only accepted {root, bridge}, so any call to it in sandboxed image loading implies missing workspaceOnly enforcement. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-Q6QF-4P5J-R25G + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.sandbox-image-tool.workspace-only-omission + - id: ghsa-qcj9-wwgw-6gm8.openclaw-workspace-trust-root-env-read + languages: + - typescript + - javascript + severity: WARNING + message: Workspace dotenv loading should not be followed by reads of OpenClaw bundled trust-root env vars; block them in the workspace loader instead. + patterns: + - pattern-either: + - pattern: | + loadWorkspaceDotEnvFile(...) + ... + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR + - pattern: | + loadWorkspaceDotEnvFile(...) + ... + process.env.OPENCLAW_BUNDLED_HOOKS_DIR + - pattern: | + loadWorkspaceDotEnvFile(...) + ... + process.env.OPENCLAW_BUNDLED_SKILLS_DIR + - pattern: | + loadWorkspaceDotEnvFile(...) + ... + process.env.OPENCLAW_BROWSER_CONTROL_MODULE + metadata: + ghsa: GHSA-QCJ9-WWGW-6GM8 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-QCJ9-WWGW-6GM8 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-workspace-trust-root-env-read + - id: ghsa-qf48-qfv4-jjm9.openclaw-extension-upload-direct-file-read-before-sandboxed-media-load + languages: + - typescript + - javascript + severity: ERROR + message: Extension upload helper reads a local file path directly before upload. Upload helpers must use sandbox-aware media/path loading (for example loadWebMedia/localRoots/workspace-scoped helpers) so tool callers cannot exfiltrate arbitrary host files through provider uploads. See GHSA-QF48-QFV4-JJM9. + metadata: + ghsa: GHSA-QF48-QFV4-JJM9 + category: security + cwe: + - CWE-22 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-QF48-QFV4-JJM9 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-extension-upload-direct-file-read-before-sandboxed-media-load + paths: + include: + - extensions/**/src/**/*.ts + exclude: + - "**/*.test.ts" + - "**/*.spec.ts" + patterns: + - pattern-inside: | + async function $F(...) { + ... + } + - pattern-either: + - pattern: const $BUF = await fs.readFile($PATH); + - pattern: const $BUF = await readFile($PATH); + - pattern: const $BUF = await fsp.readFile($PATH); + - metavariable-regex: + metavariable: $F + regex: (?i).*(resolve.*Upload.*|upload.*(Image|File|Media|Attachment)|.*UploadInput.*|send.*(Image|File|Media|Attachment)).* + - pattern-not-inside: | + ... + loadWebMedia(...) + ... + - pattern-not-inside: | + ... + resolveStrictExistingPathsWithinRoot(...) + ... + - pattern-not-inside: | + ... + resolveExistingPathsWithinRoot(...) + ... + - id: ghsa-qpjj-47vm-64pj.openclaw-browser-control-routes-missing-auth-middleware + languages: + - typescript + severity: ERROR + message: Browser control routes are registered without installBrowserAuthMiddleware(). See GHSA-QPJJ-47VM-64PJ. + metadata: + ghsa: GHSA-QPJJ-47VM-64PJ + category: security + cwe: + - CWE-306 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-QPJJ-47VM-64PJ + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-browser-control-routes-missing-auth-middleware + paths: + include: + - extensions/browser/src/server.ts + - src/browser/server.ts + patterns: + - pattern: registerBrowserRoutes(...) + - pattern-not-inside: | + ... + installBrowserAuthMiddleware(...) + ... + - id: ghsa-qqq7-4hxc-x63c.openclaw-trusted-tool-media-alias-bypass + languages: + - typescript + - javascript + severity: ERROR + message: Exact raw-name registration guards are missing before trusted tool MEDIA paths are preserved. + patterns: + - pattern-either: + - pattern: | + if (isToolResultMediaTrusted($TOOL, $RESULT)) { + ... + return $MEDIA; + } + - pattern: | + if (isToolResultMediaTrusted($TOOL, $RESULT)) return $MEDIA; + - pattern-not-inside: | + if (isToolResultMediaTrusted($TOOL, $RESULT)) { + ... + if ($NAMES !== undefined) { + ... + if (!$RAW || !$NAMES.has($RAW)) { + ... + return ...; + } + } + ... + } + paths: + include: + - src/**/*.ts + - .artifacts/ghsa-detector-review-runs/**/*.ts + metadata: + ghsa: GHSA-QQQ7-4HXC-X63C + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-QQQ7-4HXC-X63C + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-trusted-tool-media-alias-bypass + - id: ghsa-qxgf-hmcj-3xw3.raw-image-download-fetch-without-ssrf-guard + languages: + - typescript + - javascript + severity: ERROR + message: Image download helper uses raw fetch instead of a shared SSRF-guarded fetch path. + patterns: + - pattern-either: + - pattern: | + async function $F(...){ + ... + const $RESP = await fetch($URL, ...); + ... + } + - pattern: | + const $F = async (...) => { + ... + const $RESP = await fetch($URL, ...); + ... + } + - metavariable-regex: + metavariable: $F + regex: .*(fetchImage|downloadImage|loadImage|download).* + - pattern-not-inside: | + const { response: $RESP, ...$REST } = await fetchWithSsrFGuard(...) + metadata: + ghsa: GHSA-QXGF-HMCJ-3XW3 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-QXGF-HMCJ-3XW3 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: raw-image-download-fetch-without-ssrf-guard + - id: ghsa-r294-2894-92j3.exported-session-html-unsafe-marked-setup + languages: + - javascript + message: Exported session viewer configures Marked renderer callbacks without an html() raw-HTML escape handler; review for stored XSS from untrusted markdown/metadata. + severity: ERROR + patterns: + - pattern: | + marked.use({ + ..., + renderer: { + ..., + }, + ..., + }) + - pattern-not: | + marked.use({ + ..., + renderer: { + ..., + html($TOKEN) { + return escapeHtml(...); + }, + ..., + }, + ..., + }) + - pattern-not: | + marked.use({ + ..., + renderer: { + ..., + html($TOKEN) { + return escapeHtmlTags(...); + }, + ..., + }, + ..., + }) + metadata: + category: security + cwe: + - CWE-79 + ghsa: GHSA-R294-2894-92J3 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-R294-2894-92J3 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: exported-session-html-unsafe-marked-setup + - id: ghsa-r5h9-vjqc-hq3r.nextcloud-talk-allowlist-display-name-match + languages: + - typescript + - javascript + message: Nextcloud Talk allowlist checks should compare stable senderId, not mutable senderName/display name. + severity: ERROR + patterns: + - pattern-either: + - pattern: | + resolveNextcloudTalkAllowlistMatch({ + ..., + senderName: $NAME, + ... + }) + - pattern: | + resolveNextcloudTalkGroupAllow({ + ..., + senderName: $NAME, + ... + }) + metadata: + ghsa: GHSA-R5H9-VJQC-HQ3R + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-R5H9-VJQC-HQ3R + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: nextcloud-talk-allowlist-display-name-match + - id: ghsa-rchv-x836-w7xp.openclaw-dashboard-auth-query-credentials + message: Dashboard gateway credentials are being placed into URL query parameters. Use URL fragments/session-only handoff instead, and never propagate the gateway password into browser URLs. See GHSA-RCHV-X836-W7XP. + severity: ERROR + languages: + - swift + metadata: + ghsa: GHSA-RCHV-X836-W7XP + category: security + cwe: + - CWE-598 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-RCHV-X836-W7XP + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-dashboard-auth-query-credentials + paths: + include: + - apps/macos/Sources/OpenClaw/**/*.swift + pattern-regex: \bqueryItems\s*\.\s*append\s*\(\s*URLQueryItem\s*\(\s*name:\s*"(?:token|password)" + - id: ghsa-rhfg-j8jq-7v2h.openclaw.extension-configurable-baseurl-raw-fetch-bypasses-ssrf-guard + languages: + - typescript + - javascript + severity: ERROR + mode: taint + message: | + Channel/provider extension lets an operator/account-configurable URL flow into a raw HTTP client without going through fetchWithSsrFGuard. The configured value can point to a private-network destination (RFC1918, 169.254.169.254 metadata, loopback). See GHSA-RHFG-J8JQ-7V2H. Replace with fetchWithSsrFGuard(...) and thread the account's allowPrivateNetwork through an SsrFPolicy. + TRIAGE NOTE: This rule only fires when baseUrl/endpoint flows from a params/config/opts-shaped object (i.e. operator- or LLM-supplied input). Hardcoded constants (e.g. `this.baseUrl = "https://api.twilio.com/..."`) are NOT flagged because they are not attacker-controllable. If you see a finding where the URL is verifiably a compile-time constant, it is a false positive — please add a nosem comment and confirm the constant cannot be overridden at runtime. + metadata: + category: security + cwe: + - CWE-918 + ghsas: + - GHSA-RHFG-J8JQ-7V2H + ghsa: GHSA-RHFG-J8JQ-7V2H + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-RHFG-J8JQ-7V2H + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.extension-configurable-baseurl-raw-fetch-bypasses-ssrf-guard + paths: + include: + - extensions/**/*.ts + - extensions/**/*.tsx + - extensions/**/*.js + - extensions/**/*.mjs + exclude: + - extensions/**/*.test.ts + - extensions/**/*.test.tsx + - extensions/**/test-harness* + - extensions/**/*.spec.ts + pattern-sources: + - patterns: + - pattern: | + const { ..., $URL, ... } = $PARAMS; + - metavariable-regex: + metavariable: $URL + regex: ^(baseUrl|serverUrl|apiBaseUrl|endpoint|host)$|^[a-z][a-zA-Z0-9]*(BaseUrl|BaseURL|ServerUrl|ServerURL|ApiBaseUrl|Endpoint|Host)$ + - pattern-not: | + const { ..., $URL, ... } = { ... }; + - pattern-not: | + const { ..., $URL, ... } = $OBJ[$KEY]; + - patterns: + - pattern: $PARAMS.$URL + - metavariable-regex: + metavariable: $URL + regex: ^(baseUrl|serverUrl|apiBaseUrl|endpoint|host)$|^[a-z][a-zA-Z0-9]*(BaseUrl|BaseURL|ServerUrl|ServerURL|ApiBaseUrl|Endpoint|Host)$ + - metavariable-regex: + metavariable: $PARAMS + regex: ^(params|config|options|opts|input|args|cfg|settings|account|credentials|ext)$|^[a-z][a-zA-Z0-9]*(Params|Config|Options|Opts|Input|Args|Cfg|Settings|Account|Credentials|Ext)$ + pattern-sanitizers: + - pattern: fetchWithSsrFGuard(...) + - pattern: fetchWithSsrfGuard(...) + - pattern: $X.fetchWithSsrFGuard(...) + - pattern: ssrFCheck(...) + - pattern: validateUrlForSsrF(...) + pattern-sinks: + - pattern-either: + - pattern: fetch($URL, ...) + - pattern: undiciFetch($URL, ...) + - pattern: undici.fetch($URL, ...) + - pattern: $CLIENT.fetch($URL, ...) + - pattern: axios($URL, ...) + - pattern: axios.$M($URL, ...) + - pattern: got($URL, ...) + - pattern: got.$M($URL, ...) + - pattern: ky($URL, ...) + - pattern: wretch($URL) + - pattern: superagent.$M($URL) + - pattern: needle.$M($URL, ...) + - pattern: phin($URL, ...) + - pattern: http.request($URL, ...) + - pattern: https.request($URL, ...) + - pattern: http.get($URL, ...) + - pattern: https.get($URL, ...) + - id: ghsa-rm2p-j3r7-4x4j.slack-reaction-handler-missing-system-event-authorization + languages: + - typescript + - javascript + severity: ERROR + message: Slack reaction ingress that enqueues system events should first resolve authorizeAndResolveSlackSystemEventContext and bail out when authorization fails. + patterns: + - pattern-either: + - pattern: | + ctx.app.event("reaction_added", async (...) => { + ... + enqueueSystemEvent($TEXT, $OPTS) + ... + }) + - pattern: | + ctx.app.event("reaction_removed", async (...) => { + ... + enqueueSystemEvent($TEXT, $OPTS) + ... + }) + - pattern-not: | + ctx.app.event($EVENT, async (...) => { + ... + const $CTX = await authorizeAndResolveSlackSystemEventContext(...) + if (!$CTX) { + ... + } + ... + enqueueSystemEvent($TEXT, $OPTS) + ... + }) + metadata: + ghsa: GHSA-RM2P-J3R7-4X4J + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-RM2P-J3R7-4X4J + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: slack-reaction-handler-missing-system-event-authorization + - id: ghsa-rqp8-q22p-5j9q.openclaw-multi-account-plugin-route-replacement + languages: + - typescript + - javascript + severity: ERROR + message: "Plugin registers an account-scoped webhook route with replaceExisting: true. Duplicate webhook paths can replace another account's handler and collapse per-account policy context. See GHSA-RQP8-Q22P-5J9Q." + metadata: + ghsa: GHSA-RQP8-Q22P-5J9Q + category: security + cwe: + - CWE-284 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-RQP8-Q22P-5J9Q + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-multi-account-plugin-route-replacement + paths: + include: + - extensions/**/src/**/*.ts + pattern: | + registerPluginHttpRoute({ + ..., + replaceExisting: true, + ..., + accountId: $ACCOUNT, + ..., + }) + - id: ghsa-rvqr-hrcc-j9vv.unresolved-discovery-routing-from-txt-hints + languages: + - typescript + - javascript + severity: WARNING + message: Discovery routing falls back from resolved host/port fields to TXT-only host or port hints, which can let unresolved metadata steer the chosen endpoint. + patterns: + - pattern-either: + - patterns: + - pattern-either: + - pattern: | + $HOST = $BEACON.host || $BEACON.tailnetDns || $BEACON.lanHost + - pattern: | + $HOST = $BEACON.host || $BEACON.lanHost || $BEACON.tailnetDns + - pattern: | + $HOST = $BEACON.tailnetDns || $BEACON.lanHost || $BEACON.host + - pattern: | + $HOST = $BEACON.tailnetDns || $BEACON.host || $BEACON.lanHost + - pattern: | + $HOST = $BEACON.lanHost || $BEACON.host || $BEACON.tailnetDns + - pattern: | + $HOST = $BEACON.lanHost || $BEACON.tailnetDns || $BEACON.host + - pattern: | + return $BEACON.host || $BEACON.tailnetDns || $BEACON.lanHost + - pattern: | + return $BEACON.host || $BEACON.lanHost || $BEACON.tailnetDns + - pattern: | + return $BEACON.tailnetDns || $BEACON.lanHost || $BEACON.host + - pattern: | + return $BEACON.tailnetDns || $BEACON.host || $BEACON.lanHost + - pattern: | + return $BEACON.lanHost || $BEACON.host || $BEACON.tailnetDns + - pattern: | + return $BEACON.lanHost || $BEACON.tailnetDns || $BEACON.host + - metavariable-regex: + metavariable: $HOST + regex: .*(host|Host|targetHost).* + - patterns: + - pattern-either: + - pattern: | + $PORT = $BEACON.port ?? $BEACON.gatewayPort ?? $DEFAULT + - pattern: | + return $BEACON.port ?? $BEACON.gatewayPort ?? $DEFAULT + - metavariable-regex: + metavariable: $PORT + regex: .*(port|Port).* + metadata: + confidence: medium + category: security + bug-family: unresolved-discovery-routing + ghsa: GHSA-RVQR-HRCC-J9VV + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-RVQR-HRCC-J9VV + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: unresolved-discovery-routing-from-txt-hints + - id: ghsa-rwj8-p9vq-25gv.bluebubbles-media-read-without-root-allowlist + languages: + - typescript + - javascript + severity: ERROR + message: A non-HTTP media source is converted with resolveLocalMediaPath(...) and then read from disk without first enforcing a root allowlist validator such as assertLocalMediaPathAllowed. + metadata: + ghsa: GHSA-RWJ8-P9VQ-25GV + cwe: CWE-22 + category: security + confidence: medium + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-RWJ8-P9VQ-25GV + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: bluebubbles-media-read-without-root-allowlist + patterns: + - pattern: | + const $LOCAL = resolveLocalMediaPath(...); + ... + const $FS = await import("node:fs/promises"); + ... + const $DATA = await $FS.readFile($LOCAL); + - pattern-not: | + const $SAFE = await assertLocalMediaPathAllowed(...); + ... + const $DATA = $SAFE.data; + - id: ghsa-v3j7-34xh-6g3w.loopback-cdp-probe-copies-relay-auth-headers + message: Loopback CDP discovery/probe code should not merge extension relay auth headers into generic HTTP headers. This can leak a gateway-derived relay token to any local /json/version listener. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + const $RELAY = $GET($URL); + ... + const $MERGED = { ...$RELAY, ...$HEADERS }; + - pattern: | + const $MERGED = { ...$GET($URL), ...$HEADERS }; + - metavariable-regex: + metavariable: $GET + regex: ^(getChromeExtensionRelayAuthHeaders|.*relay.*Headers|.*relay.*Auth.*)$ + metadata: + category: security + confidence: medium + ghsa: GHSA-V3J7-34XH-6G3W + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-V3J7-34XH-6G3W + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: loopback-cdp-probe-copies-relay-auth-headers + - id: ghsa-v3qc-wrwx-j3pw.openclaw.gateway.config-write.missing-protected-path-guard + languages: + - typescript + - javascript + severity: ERROR + message: Sensitive gateway config writes should call assertGatewayConfigMutationAllowed before forwarding config.apply/config.patch. + patterns: + - pattern-either: + - pattern: | + if ($ACTION === "config.apply") { + ... + $RESULT = await callGatewayTool("config.apply", $OPTS, $PARAMS); + ... + } + - pattern: | + if ($ACTION === "config.patch") { + ... + $RESULT = await callGatewayTool("config.patch", $OPTS, $PARAMS); + ... + } + - pattern-not-inside: | + if ($ACTION === $KIND) { + ... + assertGatewayConfigMutationAllowed(...); + ... + $RESULT = await callGatewayTool($KIND, $OPTS, $PARAMS); + ... + } + metadata: + ghsa: GHSA-V3QC-WRWX-J3PW + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-V3QC-WRWX-J3PW + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.gateway.config-write.missing-protected-path-guard + - id: ghsa-v6x2-2qvm-6gv8.prompt-hash-secret-falls-back-to-gateway-auth-token + languages: + - typescript + - javascript + severity: WARNING + message: Prompt or owner-display hashing secret falls back to gateway auth or remote tokens, reusing an auth secret across trust domains. + metadata: + category: security + confidence: medium + ghsa: GHSA-V6X2-2QVM-6GV8 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-V6X2-2QVM-6GV8 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: prompt-hash-secret-falls-back-to-gateway-auth-token + patterns: + - pattern-either: + - pattern: | + ownerDisplaySecret: $CFG?.commands?.ownerDisplaySecret ?? $CFG?.gateway?.auth?.token ?? $CFG?.gateway?.remote?.token + - pattern: | + ownerDisplaySecret: $CFG?.commands?.ownerDisplaySecret || $CFG?.gateway?.auth?.token || $CFG?.gateway?.remote?.token + - pattern: | + $CFG?.commands?.ownerDisplaySecret?.trim() || $CFG?.gateway?.auth?.token?.trim() || $CFG?.gateway?.remote?.token?.trim() + - pattern: | + $X.hashSecret || $X.auth?.token || $X.remoteToken + - pattern: | + $X.hashSecret || $X.auth.token || $X.remoteToken + - id: ghsa-v865-p3gq-hw6m.repeated-decode-without-limit-anomaly-check + languages: + - typescript + - javascript + severity: WARNING + message: Repeated path decode loops should enforce a decode-depth anomaly check before treating the path as safe for auth decisions. + patterns: + - pattern-inside: | + function $F(...){ + ... + } + - pattern: | + for (let $PASS = 0; $PASS < $LIMIT; $PASS++) { + ... + $NEXT = decodeURIComponent($DECODED); + ... + } + - pattern-not-inside: | + function $F(...){ + ... + $ANOMALY = decodeURIComponent($DECODED) !== $DECODED; + ... + } + metadata: + confidence: medium + rationale: catches bounded repeated-decode canonicalizers that never fail closed on leftover encodings + ghsa: GHSA-V865-P3GQ-HW6M + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-V865-P3GQ-HW6M + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: repeated-decode-without-limit-anomaly-check + - id: ghsa-v8cg-4474-49v8.openclaw-slack-system-event-missing-sender-authorization + languages: + - typescript + severity: WARNING + message: Slack member/message subtype handler enqueues a system event without first resolving sender authorization via authorizeAndResolveSlackSystemEventContext(). Unauthorized Slack users can otherwise inject trusted system events. See GHSA-V8CG-4474-49V8. + metadata: + ghsa: GHSA-V8CG-4474-49V8 + category: security + cwe: + - CWE-862 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-V8CG-4474-49V8 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-slack-system-event-missing-sender-authorization + paths: + include: + - extensions/slack/src/monitor/events/members.ts + - extensions/slack/src/monitor/events/messages.ts + - src/slack/monitor/events/members.ts + - src/slack/monitor/events/messages.ts + patterns: + - pattern: enqueueSystemEvent(...) + - pattern-not-inside: | + ... + authorizeAndResolveSlackSystemEventContext(...) + ... + enqueueSystemEvent(...) + - id: ghsa-v8wv-jg3q-qwpq.outbound-media-alias-not-normalized + languages: + - typescript + - javascript + severity: ERROR + message: Sandbox media normalization or attachment hydration handles canonical media/path keys but omits mediaUrl/fileUrl aliases, which can bypass local root validation. + paths: + include: + - src/infra/outbound/*.ts + pattern-either: + - patterns: + - pattern: | + const $KEYS: Array<"media" | "path" | "filePath"> = ["media", "path", "filePath"]; + - pattern-not: | + const $ANY = ["media", "path", "filePath", "mediaUrl", "fileUrl"] + - patterns: + - pattern: | + const $MEDIA = readStringParam($ARGS, "media", { trim: false }); + ... + const $FILE = + readStringParam($ARGS, "path", { trim: false }) ?? + readStringParam($ARGS, "filePath", { trim: false }); + - pattern-not: | + readStringParam($ARGS, "mediaUrl", { trim: false }) + - pattern-not: | + readStringParam($ARGS, "fileUrl", { trim: false }) + metadata: + ghsa: GHSA-V8WV-JG3Q-QWPQ + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-V8WV-JG3Q-QWPQ + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: outbound-media-alias-not-normalized + - id: ghsa-vfg3-pqpq-93m4.openclaw-tlon-cites-before-final-auth-general + message: Cite expansion happens before a final DM or channel authorization decision. + severity: WARNING + languages: + - typescript + - javascript + patterns: + - pattern-either: + - pattern: | + const $CITED = await resolveAllCites($CONTENT); + ... + if (!isDmAllowed($SENDER, $ALLOWLIST)) { + ... + return; + } + - pattern: | + const $CITED = await resolveAllCites($CONTENT); + ... + const { mode, allowedShips } = resolveChannelAuthorization($CFG, $NEST, $SETTINGS); + ... + if (mode === "restricted") { + ... + if (!$ALLOWED.includes($SENDER)) { + ... + return; + } + } + metadata: + ghsa: GHSA-VFG3-PQPQ-93M4 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-VFG3-PQPQ-93M4 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-tlon-cites-before-final-auth-general + - id: ghsa-vfw7-6rhc-6xxg.openclaw.cli-backend-unsanitized-env-merge + languages: + - typescript + - javascript + severity: ERROR + message: Unsanitized env merge passed to child-process spawn. Workspace-config-derived env vars can override host vars and lead to code execution. Sanitize via sanitizeHostExecEnv() first. See GHSA-VFW7-6RHC-6XXG. + metadata: + ghsa: GHSA-VFW7-6RHC-6XXG + category: security + cwe: + - CWE-77 + - CWE-426 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-VFW7-6RHC-6XXG + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.cli-backend-unsanitized-env-merge + patterns: + - pattern-either: + - pattern: | + const $ENV = { ...process.env, ...$BACKEND.env }; + - pattern: | + const $ENV = Object.assign({}, process.env, $BACKEND.env); + - pattern: | + let $ENV = { ...process.env, ...$BACKEND.env }; + - pattern: | + const $ENV = { ...process.env, ...$BACKEND.env, ...$EXTRA }; + - pattern-either: + - pattern-inside: | + function $F(...) { + ... + spawn($CMD, $ARGS, { ..., env: $ENV, ... }) + ... + } + - pattern-inside: | + function $F(...) { + ... + spawnSync($CMD, $ARGS, { ..., env: $ENV, ... }) + ... + } + - pattern-inside: | + function $F(...) { + ... + spawn($CMD, $ARGS, { ..., env: $ENV }) + ... + } + - pattern-inside: | + ($CTX) => { + ... + spawn($CMD, $ARGS, { ..., env: $ENV, ... }) + ... + } + - pattern-inside: | + ($CTX) => { + ... + return spawn($CMD, $ARGS, { ..., env: $ENV, ... }) + ... + } + - pattern-inside: | + return spawn($CMD, $ARGS, { ..., env: $ENV, ... }) + - pattern-not-inside: | + $ENV = sanitizeHostExecEnv($X); + - id: ghsa-vj3g-5px3-gr46.openclaw.untrusted-feishu-key-or-skill-name-into-temp-path-join + languages: + - typescript + - javascript + severity: WARNING + message: | + Untrusted name/identifier from an external source flows into a path.join() call rooted at a temp/sandbox/skill destination, without passing through a validated containment helper. This is the bug class behind CVE-style path-traversal advisories where an attacker-controlled value escapes the intended directory. + metadata: + category: security + cwe: + - CWE-22 + ghsas: + - GHSA-VJ3G-5PX3-GR46 + - GHSA-XW4P-PW82-HQR7 + ghsa: GHSA-VJ3G-5PX3-GR46 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-VJ3G-5PX3-GR46 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw.untrusted-feishu-key-or-skill-name-into-temp-path-join + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern: $X.imageKey + - pattern: $X.fileKey + - pattern: $X.image_key + - pattern: $X.file_key + - pattern: | + const { ..., imageKey, ... } = $RESP; + - pattern: | + const { ..., fileKey, ... } = $RESP; + - pattern: | + const { ..., image_key, ... } = $RESP; + - pattern: | + const { ..., file_key, ... } = $RESP; + - pattern: $X.skill.name + - pattern: $X.skill.id + - pattern: $X.frontmatter.$Y + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: sanitizeTempFileName(...) + - pattern: withTempDownloadPath(...) + - pattern: createTempDownloadTarget(...) + - pattern: resolveSandboxPath(...) + - pattern: resolvePathWithinRoot(...) + - pattern: resolveWritablePathWithinRoot(...) + - pattern: isPathInside(...) + pattern-sinks: + - patterns: + - pattern-either: + - pattern: path.join($TMP, ...) + - pattern: path.resolve($TMP, ...) + - id: ghsa-vmqr-rc7x-3446.openclaw-safe-bins-external-helper-flag + languages: + - typescript + - javascript + severity: WARNING + message: safeBins sort profiles must deny --compress-program instead of allowing it as a value-bearing option, because the flag launches an external helper. + pattern-regex: (?s)sort\s*:\s*\{.*?valueFlags\s*:\s*\[[^\]]*["\']--compress-program["\'][^\]]*\] + metadata: + ghsa: GHSA-VMQR-RC7X-3446 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-VMQR-RC7X-3446 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-safe-bins-external-helper-flag + - id: ghsa-vvgp-4c28-m3jm.trusted-proxy-control-ui-bypass-without-operator-role + message: Trusted-proxy Control UI pairing bypass should require an explicit operator-role guard on the trustedProxyAuthOk helper. + severity: ERROR + languages: + - typescript + - javascript + patterns: + - pattern: | + const $TP = + isControlUi && + ... && + $MODE === "trusted-proxy" && + ... && + $AUTH_METHOD === "trusted-proxy"; + - pattern-not: | + const $TP = + isControlUi && + ... && + $ROLE === "operator" && + ... && + $MODE === "trusted-proxy" && + ... && + $AUTH_METHOD === "trusted-proxy"; + metadata: + ghsa: GHSA-VVGP-4C28-M3JM + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-VVGP-4C28-M3JM + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: trusted-proxy-control-ui-bypass-without-operator-role + - id: ghsa-vw3h-q6xq-jjm5.websocket-missing-maxpayload-noserver + languages: + - typescript + - javascript + severity: WARNING + message: WebSocketServer created with noServer but without maxPayload; unauthenticated oversized frames may reach application parsing. + patterns: + - pattern: | + new WebSocketServer({ + ..., + noServer: true, + ... + }) + - pattern-not: | + new WebSocketServer({ + ..., + noServer: true, + ..., + maxPayload: $MAX, + ... + }) + metadata: + category: security + cwe: + - CWE-400 + - CWE-770 + ghsa: GHSA-VW3H-Q6XQ-JJM5 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-VW3H-Q6XQ-JJM5 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: websocket-missing-maxpayload-noserver + - id: ghsa-w235-x559-36mg.docker-workspace-mount-ro-guard + languages: + - typescript + - javascript + severity: WARNING + message: Workspace bind mount uses a conditional :ro suffix tied to an extra equality check instead of directly enforcing non-rw access. + patterns: + - pattern: | + const $SUFFIX = $ACCESS === "ro" && $SRC === $OTHER ? ":ro" : ""; + - pattern-inside: | + ... + $ARGS.push("-v", `${$SRC}:$DEST${$SUFFIX}`) + metadata: + category: security + confidence: medium + cwe: CWE-250 + ghsa: GHSA-W235-X559-36MG + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-W235-X559-36MG + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: docker-workspace-mount-ro-guard + - id: ghsa-w235-x559-36mg.docker-workspace-mount-ro-guard-let + languages: + - typescript + - javascript + severity: WARNING + message: Workspace bind mount uses a conditional :ro suffix tied to an extra equality check instead of directly enforcing non-rw access. + patterns: + - pattern: | + let $SUFFIX = $ACCESS === "ro" && $SRC === $OTHER ? ":ro" : ""; + - pattern-inside: | + ... + $ARGS.push("-v", `${$SRC}:$DEST${$SUFFIX}`) + metadata: + category: security + confidence: medium + cwe: CWE-250 + ghsa: GHSA-W235-X559-36MG + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-W235-X559-36MG + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: docker-workspace-mount-ro-guard-let + - id: ghsa-w45g-5746-x9fp.openclaw-unguarded-cron-webhook-fetch + languages: + - typescript + - javascript + severity: ERROR + message: Cron webhook delivery should use fetchWithSsrFGuard instead of direct fetch to avoid SSRF bypasses. + patterns: + - pattern-inside: | + if ($TARGET && $EVENT.summary) { + ... + } + - pattern-either: + - pattern: | + fetch($TARGET.url, { + ... + }) + - pattern: | + await fetch($TARGET.url, { + ... + }) + - pattern-not-inside: | + fetchWithSsrFGuard({ + ... + }) + - metavariable-regex: + metavariable: $TARGET + regex: .*[Ww]ebhook.* + metadata: + category: security + cwe: "CWE-918: Server-Side Request Forgery (SSRF)" + ghsa: GHSA-W45G-5746-X9FP + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-W45G-5746-X9FP + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-unguarded-cron-webhook-fetch + - id: ghsa-w76h-8m22-hpgh.msteams-shared-link-canonicalization-missing + message: MSTeams attachment URLs should be canonicalized with tryBuildGraphSharesUrlForSharedLink before direct use as a download candidate URL. + severity: WARNING + languages: + - typescript + - javascript + paths: + include: + - extensions/msteams/src/attachments/*.ts + patterns: + - pattern: | + return { + ..., + url: $URL, + ... + }; + - metavariable-pattern: + metavariable: $URL + patterns: + - pattern-either: + - pattern: contentUrl + - pattern: $ATT.contentUrl + - pattern-not-inside: | + const $GRAPH = tryBuildGraphSharesUrlForSharedLink(...); + ... + return { + ..., + url: $GRAPH, + ... + }; + metadata: + category: security + cwe: CWE-918 + ghsa: GHSA-W76H-8M22-HPGH + rationale: Directly returning MSTeams attachment contentUrl as a download candidate without Graph shared-link canonicalization can bypass the intended media host restrictions. + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-W76H-8M22-HPGH + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: msteams-shared-link-canonicalization-missing + - id: ghsa-w7j5-j98m-w679.dockerfile-missing-user-directive + languages: + - dockerfile + severity: WARNING + message: Dockerfile appears to omit any runtime USER directive, so the final runtime user may stay root/default. + metadata: + ghsa: GHSA-W7J5-J98M-W679 + detector: A + category: security + cwe: CWE-250 + confidence: medium + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-W7J5-J98M-W679 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: dockerfile-missing-user-directive + paths: + include: + - "**/Dockerfile" + - "**/Dockerfile.*" + exclude: + - "**/vendor/**" + pattern-regex: (?s)\AFROM\b(?:(?!\nUSER\b).)*\Z + - id: ghsa-w9j9-w4cp-6wgr.openclaw-host-exec-manual-env-merge + languages: + - typescript + - javascript + severity: WARNING + message: Manual host-exec env merging that only blocks PATH can miss dangerous interpreter or startup env vars; use centralized host env sanitization. + patterns: + - pattern-either: + - pattern: | + for (const [$RAW, $VALUE] of Object.entries($OVERRIDES)) { + ... + const $KEY = $RAW.trim(); + ... + const $UPPER = $KEY.toUpperCase(); + ... + if ($UPPER === "PATH") { + continue; + } + ... + $MERGED[$KEY] = $VALUE; + ... + } + - pattern: | + for (const [$RAW, $VALUE] of Object.entries($OVERRIDES)) { + ... + const $UPPER = $RAW.toUpperCase(); + ... + if ($UPPER === "PATH") { + continue; + } + ... + $MERGED[$RAW] = $VALUE; + ... + } + - pattern-not-inside: | + if (isDangerousHostEnvVarName(...)) { + ... + } + - pattern-not-inside: | + if (blockedEnvKeys.has(...)) { + ... + } + - pattern-not-inside: | + if (blockedEnvPrefixes.some(($PREFIX) => ...)) { + ... + } + paths: + include: + - "**/*.ts" + - "**/*.js" + metadata: + ghsa: GHSA-W9J9-W4CP-6WGR + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-W9J9-W4CP-6WGR + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-host-exec-manual-env-merge + - id: ghsa-wh94-p5m6-mr7j.openclaw-discord-moderation-dispatch-missing-sender-permission-check + languages: + - typescript + severity: WARNING + message: Discord moderation action dispatches timeout/kick/ban without verifySenderModerationPermission(). See GHSA-WH94-P5M6-MR7J. + metadata: + ghsa: GHSA-WH94-P5M6-MR7J + category: security + cwe: + - CWE-862 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-WH94-P5M6-MR7J + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-discord-moderation-dispatch-missing-sender-permission-check + paths: + include: + - extensions/discord/src/actions/runtime.moderation.ts + - src/agents/tools/discord-actions-moderation.ts + patterns: + - pattern-either: + - pattern: timeoutMemberDiscord(...) + - pattern: kickMemberDiscord(...) + - pattern: banMemberDiscord(...) + - pattern-not-inside: | + ... + verifySenderModerationPermission(...) + ... + - id: ghsa-wj55-88gf-x564.queued-node-action-delivery-without-current-policy-recheck + message: Pending node actions are delivered from the raw queue without current-policy revalidation. + severity: WARNING + languages: + - typescript + - javascript + patterns: + - pattern: | + const $PENDING = listPendingNodeActions($NODE_ID); + - pattern-not-inside: | + const $PENDING = listPendingNodeActions($NODE_ID); + ... + const $ALLOWED = $PENDING.filter((entry) => { + ... + const $RESULT = isNodeCommandAllowed(...); + ... + }); + metadata: + category: security + ghsa: GHSA-WJ55-88GF-X564 + bug_family: stale-queued-node-actions + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-WJ55-88GF-X564 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: queued-node-action-delivery-without-current-policy-recheck + - id: ghsa-wq58-2pvg-5h4f.gateway-http-json-endpoint-missing-required-operator-method + languages: + - typescript + - javascript + severity: ERROR + message: Gateway HTTP handlers that call handleGatewayPostJsonEndpoint should set requiredOperatorMethod when the endpoint mutates chat/session state. + patterns: + - pattern-either: + - pattern: | + handleGatewayPostJsonEndpoint($REQ, $RES, { + ..., + pathname: "/v1/chat/completions", + ... + }) + - pattern: | + handleGatewayPostJsonEndpoint($REQ, $RES, { + ..., + pathname: "/v1/responses", + ... + }) + - pattern: | + const $HANDLED = await handleGatewayPostJsonEndpoint($REQ, $RES, { + ..., + pathname: "/v1/chat/completions", + ... + }) + - pattern: | + const $HANDLED = await handleGatewayPostJsonEndpoint($REQ, $RES, { + ..., + pathname: "/v1/responses", + ... + }) + - pattern-not: | + handleGatewayPostJsonEndpoint($REQ, $RES, { + ..., + requiredOperatorMethod: $METHOD, + ... + }) + - pattern-not: | + const $HANDLED = await handleGatewayPostJsonEndpoint($REQ, $RES, { + ..., + requiredOperatorMethod: $METHOD, + ... + }) + metadata: + category: authorization + confidence: medium + review: family-detector + ghsa: GHSA-WQ58-2PVG-5H4F + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-WQ58-2PVG-5H4F + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: gateway-http-json-endpoint-missing-required-operator-method + - id: ghsa-x2m8-53h4-6hch.discord-voice-ingress-missing-authorize-review + message: Discord voice ingress forwards senderIsOwner to agentCommandFromIngress without calling authorizeDiscordVoiceIngress in the same file. See GHSA-X2M8-53H4-6HCH. + severity: ERROR + languages: + - typescript + metadata: + ghsa: GHSA-X2M8-53H4-6HCH + category: authorization + cwe: + - CWE-863 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-X2M8-53H4-6HCH + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: discord-voice-ingress-missing-authorize-review + paths: + include: + - extensions/discord/src/voice/** + patterns: + - pattern: agentCommandFromIngress(...) + - pattern-not-inside: | + import { ..., authorizeDiscordVoiceIngress, ... } from "$X"; + ... + - pattern-not-inside: | + import { authorizeDiscordVoiceIngress } from "$X"; + ... + - pattern-not-inside: | + import { authorizeDiscordVoiceIngress, ... } from "$X"; + ... + - id: ghsa-x9cf-3w63-rpq9.openclaw-remote-media-direct-scp-without-source-policy + languages: + - typescript + severity: ERROR + message: Remote media staging directly scpFile()s `source` from MediaRemoteHost. The source must first be checked against remote attachment roots via isAllowedSourcePath(). See GHSA-X9CF-3W63-RPQ9. + metadata: + ghsa: GHSA-X9CF-3W63-RPQ9 + category: security + cwe: + - CWE-22 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-X9CF-3W63-RPQ9 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-remote-media-direct-scp-without-source-policy + paths: + include: + - src/auto-reply/reply/stage-sandbox-media.ts + pattern-regex: scpFile\(ctx\.MediaRemoteHost,\s*source,\s*dest\) + - id: ghsa-xhq5-45pm-2gjr.nextcloud-talk-room-name-fallback-match + message: Nextcloud Talk room authorization must match on the stable roomToken only — display names are collidable across rooms and let an attacker with create-room permission bypass the allowlist by reusing an allowed room's name. See GHSA-XHQ5-45PM-2GJR. + severity: ERROR + languages: + - typescript + - javascript + metadata: + ghsa: GHSA-XHQ5-45PM-2GJR + category: authorization + cwe: + - CWE-863 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-XHQ5-45PM-2GJR + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: nextcloud-talk-room-name-fallback-match + paths: + include: + - extensions/nextcloud-talk/src/** + patterns: + - pattern-either: + - pattern: buildChannelKeyCandidates($TOKEN, $ROOM_NAME, ...) + - pattern: buildChannelKeyCandidates($TOKEN, ..., normalizeChannelSlug($ROOM_NAME)) + - pattern: buildChannelKeyCandidates($TOKEN, ..., $ROOM_NAME, ...) + - id: ghsa-xp9r-prpg-373r.openclaw-browser-request-reset-profile-mutation + languages: + - typescript + message: browser.request mutation guards should treat POST /reset-profile like other persistent-profile mutations + severity: ERROR + patterns: + - pattern-either: + - pattern: | + if ($METHOD === "POST" && $PATH === "/profiles/create") { + ... + } + - pattern: | + if ($METHOD === "POST" && $PATH == "/profiles/create") { + ... + } + - pattern: | + if ($METHOD === "POST" && $PATH === "/profiles/create") return ...; + - pattern: | + if ($METHOD === "POST" && $PATH == "/profiles/create") return ...; + - pattern-not-inside: | + if ( + $METHOD === "POST" && + ($PATH === "/profiles/create" || $PATH === "/reset-profile") + ) { + ... + } + - pattern-not-inside: | + if ( + $METHOD == "POST" && + ($PATH == "/profiles/create" || $PATH == "/reset-profile") + ) { + ... + } + - pattern-not: | + if ($METHOD === "POST" && ($PATH === "/profiles/create" || $PATH === "/reset-profile")) { + ... + } + - pattern-not: | + if ($METHOD == "POST" && ($PATH == "/profiles/create" || $PATH == "/reset-profile")) { + ... + } + metadata: + category: security + cwe: CWE-863 + ghsa: GHSA-XP9R-PRPG-373R + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-XP9R-PRPG-373R + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: openclaw-browser-request-reset-profile-mutation + - id: ghsa-xwcj-hwhf-h378.media-fetch-logs-unredacted-url + languages: + - typescript + - javascript + severity: ERROR + message: Error messages should redact URL values before embedding them in media-fetch failures. + patterns: + - pattern-either: + - pattern: | + new MediaFetchError($CODE, `Failed to fetch media from ${$URL}: ${...}`) + - pattern: | + new MediaFetchError($CODE, `Failed to fetch media from ${$URL}${$REST}: ${...}`) + - metavariable-pattern: + metavariable: $URL + patterns: + - pattern-not: sourceUrl + - pattern-not: redactMediaUrl(...) + metadata: + category: security + ghsa: GHSA-XWCJ-HWHF-H378 + detector: A + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-XWCJ-HWHF-H378 + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: media-fetch-logs-unredacted-url + - id: ghsa-xwjm-j929-xq7c.browser-download-route-missing-resolve-writable-output-path + message: Browser download route handler calls Playwright download without resolving the path through the temp-downloads-root gate. An authenticated CLI/RPC caller can supply a path that traverses outside the temp downloads dir, leading to arbitrary file write. See GHSA-XWJM-J929-XQ7C. + severity: ERROR + languages: + - typescript + metadata: + ghsa: GHSA-XWJM-J929-XQ7C + category: security + cwe: + - CWE-22 + advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-XWJM-J929-XQ7C + detector-bucket: precise + source-run: 2026-04-17T07-37-10Z + source-rule-id: browser-download-route-missing-resolve-writable-output-path + paths: + include: + - extensions/browser/src/browser/routes/**.ts + - src/browser/routes/**.ts + patterns: + - pattern-either: + - pattern: pw.waitForDownloadViaPlaywright(...) + - pattern: pw.downloadViaPlaywright(...) + - pattern-not-inside: | + import { ..., resolveWritableOutputPathOrRespond, ... } from "$X"; + ... + - pattern-not-inside: | + import { resolveWritableOutputPathOrRespond } from "$X"; + ... + - pattern-not-inside: | + import { resolveWritableOutputPathOrRespond, ... } from "$X"; + ... + - pattern-not-inside: | + import { ..., resolvePathWithinRoot, ... } from "$X"; + ... + - pattern-not-inside: | + import { resolvePathWithinRoot } from "$X"; + ... + - pattern-not-inside: | + import { resolvePathWithinRoot, ... } from "$X"; + ... diff --git a/test/scripts/check-opengrep-rule-metadata.test.ts b/test/scripts/check-opengrep-rule-metadata.test.ts new file mode 100644 index 00000000000..ff075d3ff45 --- /dev/null +++ b/test/scripts/check-opengrep-rule-metadata.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { validateRuleMetadata } from "../../security/opengrep/check-rule-metadata.mjs"; + +const validRule = { + id: "ghsa-1234-abcd-5678.source-rule", + metadata: { + ghsa: "GHSA-1234-ABCD-5678", + "advisory-url": "https://github.com/openclaw/openclaw/security/advisories/GHSA-1234-ABCD-5678", + "detector-bucket": "precise", + "source-rule-id": "source-rule", + }, +}; + +describe("check-opengrep-rule-metadata", () => { + it("accepts GHSA-backed rules with durable source metadata", () => { + expect(validateRuleMetadata([validRule])).toEqual([]); + }); + + it("requires source metadata on every compiled rule", () => { + expect( + validateRuleMetadata([ + { + id: "ghsa-1234-abcd-5678.source-rule", + metadata: { + ghsa: "GHSA-1234-ABCD-5678", + "detector-bucket": "precise", + }, + }, + ]), + ).toEqual([ + "ghsa-1234-abcd-5678.source-rule: missing metadata.advisory-url", + "ghsa-1234-abcd-5678.source-rule: missing metadata.source-rule-id", + ]); + }); + + it("accepts non-GHSA source-backed rules with durable source metadata", () => { + expect( + validateRuleMetadata([ + { + id: "cve-2026-12345.source-rule", + metadata: { + "advisory-id": "CVE-2026-12345", + "advisory-url": "https://example.test/advisories/CVE-2026-12345", + "detector-bucket": "precise", + "source-rule-id": "source-rule", + }, + }, + ]), + ).toEqual([]); + }); + + it("keeps the source id, rule id, and GHSA advisory URL consistent", () => { + expect( + validateRuleMetadata([ + { + ...validRule, + metadata: { + ...validRule.metadata, + ghsa: "GHSA-9999-ABCD-5678", + "advisory-url": + "https://github.com/openclaw/openclaw/security/advisories/GHSA-1234-ABCD-5678", + }, + }, + ]), + ).toEqual([ + "ghsa-1234-abcd-5678.source-rule: source id in metadata (GHSA-9999-ABCD-5678) must match source id in rule id (ghsa-1234-abcd-5678)", + "ghsa-1234-abcd-5678.source-rule: metadata.advisory-url must be https://github.com/openclaw/openclaw/security/advisories/GHSA-9999-ABCD-5678", + ]); + }); +}); diff --git a/test/scripts/run-opengrep.test.ts b/test/scripts/run-opengrep.test.ts new file mode 100644 index 00000000000..ec6c4078f8f --- /dev/null +++ b/test/scripts/run-opengrep.test.ts @@ -0,0 +1,60 @@ +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { createScriptTestHarness } from "./test-helpers.js"; + +const { createTempDir } = createScriptTestHarness(); + +function git(cwd: string, ...args: string[]): string { + return execFileSync("git", args, { cwd, encoding: "utf8" }).trim(); +} + +function writeFile(filePath: string, content: string): void { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content); +} + +describe("run-opengrep.sh", () => { + it("validates the rulepack when only OpenGrep rulepack files changed", () => { + const repo = createTempDir("openclaw-run-opengrep-"); + git(repo, "init", "-q"); + git(repo, "config", "user.email", "test@example.com"); + git(repo, "config", "user.name", "Test User"); + + const scriptSource = path.resolve("scripts/run-opengrep.sh"); + writeFile(path.join(repo, "scripts/run-opengrep.sh"), fs.readFileSync(scriptSource, "utf8")); + fs.chmodSync(path.join(repo, "scripts/run-opengrep.sh"), 0o755); + writeFile(path.join(repo, "security/opengrep/precise.yml"), "rules: []\n"); + git(repo, "add", "."); + git(repo, "commit", "-qm", "initial"); + + fs.appendFileSync(path.join(repo, "security/opengrep/precise.yml"), "# changed\n"); + const argsPath = path.join(repo, "opengrep-args.txt"); + const binDir = path.join(repo, "bin"); + fs.mkdirSync(binDir); + writeFile( + path.join(binDir, "opengrep"), + [ + "#!/usr/bin/env bash", + `printf '%s\\n' "$@" > ${JSON.stringify(argsPath)}`, + "exit 0", + "", + ].join("\n"), + ); + fs.chmodSync(path.join(binDir, "opengrep"), 0o755); + + execFileSync("bash", ["scripts/run-opengrep.sh", "--changed"], { + cwd: repo, + env: { + ...process.env, + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`, + OPENCLAW_OPENGREP_BASE_REF: "HEAD", + }, + encoding: "utf8", + }); + + const args = fs.readFileSync(path.join(repo, "opengrep-args.txt"), "utf8"); + expect(args).toContain("security/opengrep/precise.yml"); + }); +});