Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/advisories.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Refresh security advisories

# Weekly refresh of static/data/openclaw-advisories.json, the data behind the
# chart and table on /security/known-vulnerabilities. Fetches OpenClaw's
# published GitHub security advisories (public data) with GITHUB_TOKEN and,
# if the file changed, opens or updates a pull request. See scripts/README.md.

on:
schedule:
- cron: '23 5 * * 1'
workflow_dispatch:

permissions:
contents: write
pull-requests: write

concurrency:
group: advisories
cancel-in-progress: false

jobs:
refresh:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: 24
- name: Fetch advisories
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node scripts/advisories.mjs --json static/data/openclaw-advisories.json > advisories-summary.md
cat advisories-summary.md >> "$GITHUB_STEP_SUMMARY"
rm advisories-summary.md
- name: Ignore a timestamp-only change
# generatedAt changes on every run; only open a PR when an advisory
# was added, removed or edited.
run: |
if git diff --quiet -I '"generatedAt"' -- static/data/openclaw-advisories.json; then
git checkout -- static/data/openclaw-advisories.json
echo "No advisory changes."
fi
- uses: peter-evans/create-pull-request@v8
with:
branch: bot/advisories-refresh
delete-branch: true
add-paths: static/data/openclaw-advisories.json
commit-message: Refresh OpenClaw security advisory data
title: Refresh OpenClaw security advisory data
body: |
Weekly refresh of `static/data/openclaw-advisories.json` from the
GitHub security advisories API (`scripts/advisories.mjs --json`).
Check the new advisories, and update the prose on
`docs/security/known-vulnerabilities.md` (batches, latest release
passed to `<MinimumSafeVersion>`) if they need it.
59 changes: 15 additions & 44 deletions docs/security/known-vulnerabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ description: OpenClaw security advisories by severity and month, every critical
keywords: [openclaw, openclaw cve, openclaw vulnerability, security advisory, ghsa, cve-2026-25253, clawhavoc, minimum safe version]
---

import {AdvisoryGenerated, AdvisoryChart, AdvisoryTable, MinimumSafeVersion} from '@site/src/components/AdvisoryTimeline';

# Known Vulnerabilities

A readable record of OpenClaw's published security advisories and the major incidents around it. Advisory numbers are as of 2026-10-18 and come from `gh api repos/openclaw/openclaw/security-advisories --paginate`. To regenerate them, run `node scripts/advisories.mjs` in this site's repository.
A readable record of OpenClaw's published security advisories and the major incidents around it. The advisory chart and table are generated from GitHub's [security advisories API](https://github.com/openclaw/openclaw/security/advisories) by `node scripts/advisories.mjs --json static/data/openclaw-advisories.json` in this site's repository, and a weekly workflow opens a pull request when the data changes. The prose sections below were last checked on 2026-10-18.

:::danger Minimum safe version: the latest stable release
:::danger[Minimum safe version: the latest stable release]
As of 2026-10-18 the latest stable release is **v2026.10.4** (published 2026-10-16). Always run the latest stable release, and update plugins with it:

```bash
Expand All @@ -20,63 +22,32 @@ openclaw plugins update --all
openclaw --version
```

There is no older "safe" version to pin. The most recent advisories (2026-09-11) have fixes up to **2026.9.2** in core and **2026.9.3** in the `@openclaw/discord` and `@openclaw/diagnostics-prometheus` plugins. Many fixes ship only in separate `@openclaw/*` channel plugins, so updating core alone is not enough.
There is no older "safe" version to pin. Many fixes ship only in separate `@openclaw/*` channel plugins, so updating core alone is not enough.

Advisories list **stable** patched versions. The extended-stable channel uses monthly patch numbers of 33 and higher ([release channels](https://docs.openclaw.ai/install/development-channels)), which don't compare directly. If you run extended-stable, check the release notes, or switch to stable.
:::

<MinimumSafeVersion latest="2026.10.4" />

---

## Advisories at a glance

**722** published repository advisories as of 2026-10-18 ([openclaw/openclaw security advisories](https://github.com/openclaw/openclaw/security/advisories)). 39 of them have a CVE ID.

| Severity | Count |
|---|---|
| Critical | 14 |
| High | 249 |
| Medium | 390 |
| Low | 69 |

| Month published | Critical | High | Medium | Low | Total |
|---|---|---|---|---|---|
| 2026-01 | 0 | 3 | 0 | 0 | 3 |
| 2026-02 | 3 | 61 | 128 | 28 | 220 |
| 2026-03 | 10 | 70 | 107 | 17 | 204 |
| 2026-04 | 1 | 39 | 53 | 15 | 108 |
| 2026-05 | 0 | 13 | 50 | 4 | 67 |
| 2026-06 | 0 | 33 | 12 | 0 | 45 |
| 2026-07 | 0 | 0 | 0 | 0 | 0 |
| 2026-08 | 0 | 0 | 0 | 0 | 0 |
| 2026-09 | 0 | 30 | 40 | 5 | 75 |
| 2026-10 (to the 18th) | 0 | 0 | 0 | 0 | 0 |
<AdvisoryGenerated />

<AdvisoryChart />

The June and September advisories were each published on one day (2026-06-30 and 2026-09-11) as coordinated disclosures of issues already fixed in released versions.

**CVSS labels.** Under CVSS 3.1 and 4.0, 9.0–10.0 is **Critical**, 7.0–8.9 **High**, 4.0–6.9 **Medium** and 0.1–3.9 **Low** ([FIRST CVSS v3.1 specification](https://www.first.org/cvss/v3.1/specification-document#Qualitative-Severity-Rating-Scale)). A score of 8.8, such as CVE-2026-25253, is **High**, not Critical.

## Critical advisories {#critical-advisories}

All 14 critical-severity advisories. None has a CVE ID. Four were published without a numeric score; their severity is GitHub's label.

| Published | Advisory | CVSS | Summary | Fixed in |
|---|---|---|---|---|
| 2026-02-14 | [GHSA-gv46-4xfq-jv58](https://github.com/openclaw/openclaw/security/advisories/GHSA-gv46-4xfq-jv58) | 9.9 | Remote code execution via node invoke approval bypass in the Gateway | 2026.2.14 |
| 2026-02-14 | [GHSA-4rj2-gpmh-qq5x](https://github.com/openclaw/openclaw/security/advisories/GHSA-4rj2-gpmh-qq5x) | 9.4 | Voice-call inbound allowlist bypass (empty caller ID plus suffix matching) | 2026.2.2 |
| 2026-02-14 | [GHSA-qrq5-wjgg-rvqw](https://github.com/openclaw/openclaw/security/advisories/GHSA-qrq5-wjgg-rvqw) | 9.3 | Path traversal in plugin installation | 2026.2.1 |
| 2026-03-12 | [GHSA-4jpw-hj22-2xmc](https://github.com/openclaw/openclaw/security/advisories/GHSA-4jpw-hj22-2xmc) | 9.9 | Pairing-scoped device tokens could mint `operator.admin` and reach node RCE | 2026.3.11 |
| 2026-03-13 | [GHSA-rqpp-rjj8-7wv8](https://github.com/openclaw/openclaw/security/advisories/GHSA-rqpp-rjj8-7wv8) | 9.9 | WebSocket shared-auth connections could self-declare elevated scopes | 2026.3.12 |
| 2026-03-24 | [GHSA-hf68-49fm-59cq](https://github.com/openclaw/openclaw/security/advisories/GHSA-hf68-49fm-59cq) | not scored | `device.pair.approve` let `operator.pairing` escalate a new device to `operator.admin` | 2026.3.22 |
| 2026-03-26 | [GHSA-9hjh-fr4f-gxc4](https://github.com/openclaw/openclaw/security/advisories/GHSA-9hjh-fr4f-gxc4) | not scored | Backend reconnect let non-admin operator scopes self-claim `operator.admin` | 2026.3.25 |
| 2026-03-26 | [GHSA-fqw4-mph7-2vr8](https://github.com/openclaw/openclaw/security/advisories/GHSA-fqw4-mph7-2vr8) | not scored | Local shared-auth reconnect widened a paired device from `operator.read` to `operator.admin` | 2026.3.25 |
| 2026-03-29 | [GHSA-hc5h-pmr3-3497](https://github.com/openclaw/openclaw/security/advisories/GHSA-hc5h-pmr3-3497) | 9.4 (CVSS 4.0) | `/pair approve` omitted caller scope subsetting and reopened pairing escalation | 2026.3.28 |
| 2026-03-29 | [GHSA-8rh7-6779-cjqq](https://github.com/openclaw/openclaw/security/advisories/GHSA-8rh7-6779-cjqq) | 9.6 | A `.env` in the working directory could inject variables and take over config | 2026.3.28 |
| 2026-03-29 | [GHSA-j7p2-qcwm-94v4](https://github.com/openclaw/openclaw/security/advisories/GHSA-j7p2-qcwm-94v4) | 9.6 | Incomplete host-env blocklist allowed supply-chain redirection via package-manager variables | 2026.3.22 |
| 2026-03-31 | [GHSA-9p3r-hh9g-5cmg](https://github.com/openclaw/openclaw/security/advisories/GHSA-9p3r-hh9g-5cmg) | 9.4 (CVSS 4.0) | Sandbox escape via a TOCTOU race in the remote FS bridge `readFile` | 2026.3.31 |
| 2026-03-31 | [GHSA-g5cg-8x5w-7jpm](https://github.com/openclaw/openclaw/security/advisories/GHSA-g5cg-8x5w-7jpm) | 9.5 (CVSS 4.0) | Heartbeat context inheritance bypassed the sandbox via `senderIsOwner` escalation | 2026.3.31 |
| 2026-04-16 | [GHSA-xh72-v6v9-mwhc](https://github.com/openclaw/openclaw/security/advisories/GHSA-xh72-v6v9-mwhc) | not scored | Feishu webhook and card-action validation did not fail closed | 2026.4.15 |

Scores are CVSS 3.1 unless marked. Also notable from February: [GHSA-g6q9-8fvw-f7rf](https://github.com/openclaw/openclaw/security/advisories/GHSA-g6q9-8fvw-f7rf) (CVE-2026-26322, High), a second `gatewayUrl` override issue in the Gateway tool, fixed in 2026.2.14.
The table starts filtered to critical-severity advisories, newest first. Pick another severity, or **All**, to see the rest; search matches the summary, GHSA and CVE IDs and the fixed-in versions. Click a column heading to sort.

<AdvisoryTable initialSeverity="critical" />

Also notable from February: [GHSA-g6q9-8fvw-f7rf](https://github.com/openclaw/openclaw/security/advisories/GHSA-g6q9-8fvw-f7rf) (CVE-2026-26322, High), a second `gatewayUrl` override issue in the Gateway tool, fixed in 2026.2.14.

---

Expand Down
27 changes: 27 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@ add `name -> { maintainers, repository }` (npm) or `{ note }` (pypi/clawhub).
A trailing `*` matches a prefix such as `@openclaw/*`. Never allowlist a name
just because it exists on the registry.

## Security advisory data (`advisories.mjs`)

`/security/known-vulnerabilities` renders its chart, advisory table and
"minimum safe version" callout from `static/data/openclaw-advisories.json`
(component: `src/components/AdvisoryTimeline.tsx`). The JSON holds only public
advisory fields: `generatedAt`, `repo`, and per advisory `ghsa_id`, `cve_id`,
`severity`, `cvss` (3.1 score, else 4.0, with `cvss_version`), `summary`,
`published_at`, `patched` (first patched versions of the core package,
including the old `clawdbot` name), `patched_other` (plugins and apps, as
`name version`) and `html_url`. One advisory per line keeps diffs readable.

```bash
# needs `gh` logged in, or GITHUB_TOKEN set; prints a Markdown summary too
node scripts/advisories.mjs --json static/data/openclaw-advisories.json
# from a saved dump (JSON array or JSONL) instead of the API
node scripts/advisories.mjs --file adv.jsonl --json static/data/openclaw-advisories.json
```

`.github/workflows/advisories.yml` runs this every Monday (and on manual
dispatch) with the workflow's `GITHUB_TOKEN`. If any advisory changed (a
`generatedAt`-only change is ignored), it opens or updates a PR from
`bot/advisories-refresh` via `peter-evans/create-pull-request`. When merging,
check whether the page's prose (batch sections, the latest release passed to
`<MinimumSafeVersion latest="...">`) needs updating too. PRs opened with
`GITHUB_TOKEN` don't trigger other workflows; re-run the snippet check by hand
or push a commit to the branch if needed.

## Link check

`.github/workflows/links.yml` runs lychee weekly over external links in
Expand Down
87 changes: 85 additions & 2 deletions scripts/advisories.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
// node scripts/advisories.mjs # fetch with `gh api` (needs gh logged in)
// GITHUB_TOKEN=... node scripts/advisories.mjs # fetch with the REST API instead
// node scripts/advisories.mjs --file adv.json # read a saved JSON array / JSONL dump
// node scripts/advisories.mjs --json static/data/openclaw-advisories.json
// # also write the compact data file
//
// Prints: totals, counts by severity, counts by publication month, and a
// table of every critical-severity advisory. No dependencies.
// table of every critical-severity advisory. With --json, also writes the
// public fields of every advisory to a compact JSON file that the
// AdvisoryTimeline component on /security/known-vulnerabilities renders.
// No dependencies.

import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";

const REPO = "openclaw/openclaw";
const SEVERITIES = ["critical", "high", "medium", "low"];
Expand Down Expand Up @@ -68,6 +74,76 @@ function cvssLabel(score) {
return "None";
}

// Former names of the core package; their fixes count as core fixes.
const CORE_PACKAGES = new Set(["openclaw", "clawdbot", "moltbot", "openclaw/openclaw", "clawdbot/clawdbot"]);

function cleanVersion(v) {
if (!v || v === "None") return null;
return String(v).replace(/^>=\s*v?/, "").trim() || null;
}

// Compact, public-only record for the site's data file.
function compact(a) {
const core = new Set();
const other = new Set();
for (const v of a.vulnerabilities ?? []) {
const name = (v.package?.name ?? "").trim();
const patched = cleanVersion(v.patched_versions);
if (CORE_PACKAGES.has(name.toLowerCase())) {
if (patched) core.add(patched);
} else if (name || patched) {
other.add(`${name || "?"} ${patched ?? "(no patched version listed)"}`.trim());
}
}
const v3 = a.cvss_severities?.cvss_v3?.score ?? null;
const v4 = a.cvss_severities?.cvss_v4?.score ?? null;
const legacy = a.cvss?.score ?? null;
const cvss = v3 ?? (v4 != null ? v4 : legacy || null);
return {
ghsa_id: a.ghsa_id,
cve_id: a.cve_id ?? null,
severity: a.severity,
cvss,
cvss_version: v3 != null ? "3.1" : v4 != null ? "4.0" : null,
summary: String(a.summary ?? "").replace(/\s+/g, " ").trim(),
published_at: a.published_at,
patched: [...core].sort(compareVersions),
patched_other: [...other].sort(),
html_url: a.html_url,
};
}

function compareVersions(a, b) {
const pa = a.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
const pb = b.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
if (d) return d;
}
return 0;
}

function writeJson(path, advisories) {
const records = advisories
.map(compact)
.sort((x, y) => (y.published_at ?? "").localeCompare(x.published_at ?? "") || x.ghsa_id.localeCompare(y.ghsa_id));
const data = { generatedAt: new Date().toISOString(), repo: REPO, advisories: records };
mkdirSync(dirname(path), { recursive: true });
// One advisory per line keeps weekly diffs readable.
const body = [
"{",
` "generatedAt": ${JSON.stringify(data.generatedAt)},`,
` "repo": ${JSON.stringify(data.repo)},`,
' "advisories": [',
records.map((r) => " " + JSON.stringify(r)).join(",\n"),
" ]",
"}",
"",
].join("\n");
writeFileSync(path, body);
console.error(`wrote ${records.length} advisories to ${path}`);
}

const esc = (s) => String(s ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();

async function main() {
Expand All @@ -79,6 +155,13 @@ async function main() {

advisories = advisories.filter((a) => a.state === "published" || a.state === undefined);

const jsonArg = process.argv.indexOf("--json");
if (jsonArg !== -1) {
const out = process.argv[jsonArg + 1];
if (!out || out.startsWith("--")) throw new Error("--json needs a path");
writeJson(out, advisories);
}

const bySeverity = Object.fromEntries(SEVERITIES.map((s) => [s, 0]));
const byMonth = new Map();
for (const a of advisories) {
Expand Down
Loading
Loading