mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(exec): enforce allowlist argument patterns (#75143)
* fix(exec): enforce allowlist argument patterns * fix(exec): document argPattern allowlist field * Add CHANGELOG entry for #75143 cross-platform argPattern enforcement --------- Co-authored-by: Devin Robison <drobison@nvidia.com> Co-authored-by: Devin Robison <drobison00@users.noreply.github.com>
This commit is contained in:
@@ -372,6 +372,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/sessions: after embedded Pi runs, append assistant-visible reply text to session JSONL only when Pi did not already persist an equivalent tail assistant entry, without re-mirroring the user prompt Pi owns. Fixes #77823. (#77839) Thanks @neeravmakwana.
|
||||
- Plugins/CLI: load the install-records ledger when listing channel-catalog entries, so npm-installed third-party channel plugins resolve through `openclaw channels login`/`channels add` instead of failing with `Unsupported channel`. (#77269) Thanks @pumpkinxing1.
|
||||
- Memory wiki/Security: enforce session visibility on shared-memory `wiki_search` and `wiki_get` so sandboxed subagents cannot read transcript content from sibling or parent sessions. Fixes GHSA-72fw-cqh5-f324. Thanks @zsxsoft.
|
||||
- Exec approvals: enforce allowlist `argPattern` argument restrictions on Linux and macOS as well as Windows, so an entry like `{ pattern: "python3", argPattern: "^safe\\.py$" }` no longer silently relaxes to a path-only match on non-Windows hosts. (#75143) Thanks @eleqtrizit.
|
||||
|
||||
## 2026.5.3-1
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ automatically.
|
||||
| ---------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------- |
|
||||
| Goal | Auto-allow narrow stdin filters | Explicitly trust specific executables |
|
||||
| Match type | Executable name + safe-bin argv policy | Resolved executable path glob, or bare command-name glob for PATH-invoked commands |
|
||||
| Argument scope | Restricted by safe-bin profile and literal-token rules | Path match only; arguments are otherwise your responsibility |
|
||||
| Argument scope | Restricted by safe-bin profile and literal-token rules | Path match by default; optional `argPattern` can restrict parsed argv |
|
||||
| Typical examples | `head`, `tail`, `tr`, `wc` | `jq`, `python3`, `node`, `ffmpeg`, custom CLIs |
|
||||
| Best use | Low-risk text transforms in pipelines | Any tool with broader behavior or side effects |
|
||||
|
||||
|
||||
@@ -299,14 +299,52 @@ Examples:
|
||||
- `~/.local/bin/*`
|
||||
- `/opt/homebrew/bin/rg`
|
||||
|
||||
Each allowlist entry tracks:
|
||||
### Restricting arguments with argPattern
|
||||
|
||||
| Field | Meaning |
|
||||
| ------------------ | -------------------------------- |
|
||||
| `id` | Stable UUID used for UI identity |
|
||||
| `lastUsedAt` | Last-used timestamp |
|
||||
| `lastUsedCommand` | Last command that matched |
|
||||
| `lastResolvedPath` | Last resolved binary path |
|
||||
Add `argPattern` when an allowlist entry should match a binary and a
|
||||
specific argument shape. OpenClaw evaluates the regular expression
|
||||
against the parsed command arguments, excluding the executable token
|
||||
(`argv[0]`). For hand-authored entries, arguments are joined with a
|
||||
single space, so anchor the pattern when you need an exact match.
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"agents": {
|
||||
"main": {
|
||||
"allowlist": [
|
||||
{
|
||||
"pattern": "python3",
|
||||
"argPattern": "^safe\\.py$"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That entry allows `python3 safe.py`; `python3 other.py` is an allowlist
|
||||
miss. If a path-only entry for the same binary is also present, unmatched
|
||||
arguments can still fall back to that path-only entry. Omit the path-only
|
||||
entry when the goal is to restrict the binary to the declared arguments.
|
||||
|
||||
Entries saved by approval flows can use an internal separator format for
|
||||
exact argv matching. Prefer the UI or approval flow to regenerate those
|
||||
entries instead of hand-editing the encoded value. If OpenClaw cannot
|
||||
parse argv for a command segment, entries with `argPattern` do not match.
|
||||
|
||||
Each allowlist entry supports:
|
||||
|
||||
| Field | Meaning |
|
||||
| ------------------ | ------------------------------------------------------------- |
|
||||
| `pattern` | Resolved binary path glob or bare command-name glob |
|
||||
| `argPattern` | Optional argv regex; omitted entries are path-only |
|
||||
| `id` | Stable UUID used for UI identity |
|
||||
| `source` | Entry source, such as `allow-always` |
|
||||
| `commandText` | Command text captured when an approval flow created the entry |
|
||||
| `lastUsedAt` | Last-used timestamp |
|
||||
| `lastUsedCommand` | Last command that matched |
|
||||
| `lastResolvedPath` | Last resolved binary path |
|
||||
|
||||
## Auto-allow skill CLIs
|
||||
|
||||
|
||||
@@ -43,12 +43,17 @@ describe("exec allowlist matching", () => {
|
||||
expect(matchAllowlist([{ pattern: "rg" }], absoluteResolution)).toBeNull();
|
||||
});
|
||||
|
||||
it("honors Windows argPattern checks for bare command-name matches", () => {
|
||||
const entries = [{ pattern: "rg", argPattern: "^--json$" }];
|
||||
it.each(["linux", "darwin", "win32"])(
|
||||
"honors argPattern checks for bare command-name matches on %s",
|
||||
(platform) => {
|
||||
const entries = [{ pattern: "rg", argPattern: "^--json$" }];
|
||||
|
||||
expect(matchAllowlist(entries, baseResolution, ["rg", "--json"], "win32")?.pattern).toBe("rg");
|
||||
expect(matchAllowlist(entries, baseResolution, ["rg", "--files"], "win32")).toBeNull();
|
||||
});
|
||||
expect(matchAllowlist(entries, baseResolution, ["rg", "--json"], platform)?.pattern).toBe(
|
||||
"rg",
|
||||
);
|
||||
expect(matchAllowlist(entries, baseResolution, ["rg", "--files"], platform)).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it("matches bare wildcard patterns against arbitrary resolved executables", () => {
|
||||
const cases = [
|
||||
|
||||
@@ -881,12 +881,6 @@ describe("windowsEscapeArg", () => {
|
||||
});
|
||||
|
||||
describe("matchAllowlist with argPattern", () => {
|
||||
// argPattern matching is Windows-only; skip this suite on other platforms.
|
||||
if (process.platform !== "win32") {
|
||||
it.skip("argPattern tests are Windows-only", () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
const resolution = {
|
||||
rawExecutable: "python3",
|
||||
resolvedPath: "/usr/bin/python3",
|
||||
@@ -907,25 +901,57 @@ describe("matchAllowlist with argPattern", () => {
|
||||
expect(matchAllowlist(entries, resolution, ["python3", "a.py", "--verbose"])).toBeNull();
|
||||
});
|
||||
|
||||
it("prefers argPattern match over path-only match", () => {
|
||||
it.each(["linux", "darwin"])("enforces argPattern on %s", (platform) => {
|
||||
const entries: ExecAllowlistEntry[] = [
|
||||
{ pattern: "/usr/bin/python3" },
|
||||
{ pattern: "/usr/bin/python3", argPattern: "^a\\.py$" },
|
||||
{ pattern: "/usr/bin/python3", argPattern: "^safe\\.py$" },
|
||||
];
|
||||
const match = matchAllowlist(entries, resolution, ["python3", "a.py"]);
|
||||
expect(match).toBeTruthy();
|
||||
expect(match!.argPattern).toBe("^a\\.py$");
|
||||
expect(matchAllowlist(entries, resolution, ["python3", "safe.py"], platform)).toBeTruthy();
|
||||
expect(matchAllowlist(entries, resolution, ["python3", "-c", "print(1)"], platform)).toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to path-only match when argPattern doesn't match", () => {
|
||||
const entries: ExecAllowlistEntry[] = [
|
||||
{ pattern: "/usr/bin/python3" },
|
||||
{ pattern: "/usr/bin/python3", argPattern: "^a\\.py$" },
|
||||
];
|
||||
const match = matchAllowlist(entries, resolution, ["python3", "b.py"]);
|
||||
expect(match).toBeTruthy();
|
||||
expect(match!.argPattern).toBeUndefined();
|
||||
});
|
||||
it.each(["linux", "darwin", "win32"])(
|
||||
"prefers argPattern match over path-only match on %s",
|
||||
(platform) => {
|
||||
const entries: ExecAllowlistEntry[] = [
|
||||
{ pattern: "/usr/bin/python3" },
|
||||
{ pattern: "/usr/bin/python3", argPattern: "^a\\.py$" },
|
||||
];
|
||||
const match = matchAllowlist(entries, resolution, ["python3", "a.py"], platform);
|
||||
expect(match).toBeTruthy();
|
||||
expect(match!.argPattern).toBe("^a\\.py$");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["linux", "darwin", "win32"])(
|
||||
"falls back to path-only match when argPattern does not match on %s",
|
||||
(platform) => {
|
||||
const entries: ExecAllowlistEntry[] = [
|
||||
{ pattern: "/usr/bin/python3" },
|
||||
{ pattern: "/usr/bin/python3", argPattern: "^a\\.py$" },
|
||||
];
|
||||
const match = matchAllowlist(entries, resolution, ["python3", "b.py"], platform);
|
||||
expect(match).toBeTruthy();
|
||||
expect(match!.argPattern).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["linux", "darwin", "win32"])(
|
||||
"requires argv before matching argPattern entries on %s",
|
||||
(platform) => {
|
||||
const restrictedEntries: ExecAllowlistEntry[] = [
|
||||
{ pattern: "/usr/bin/python3", argPattern: "^a\\.py$" },
|
||||
];
|
||||
expect(matchAllowlist(restrictedEntries, resolution, undefined, platform)).toBeNull();
|
||||
|
||||
const mixedEntries: ExecAllowlistEntry[] = [
|
||||
{ pattern: "/usr/bin/python3", argPattern: "^a\\.py$" },
|
||||
{ pattern: "/usr/bin/python3" },
|
||||
];
|
||||
const fallback = matchAllowlist(mixedEntries, resolution, undefined, platform);
|
||||
expect(fallback).toBeTruthy();
|
||||
expect(fallback!.argPattern).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("handles invalid regex gracefully", () => {
|
||||
const entries: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/python3", argPattern: "[invalid" }];
|
||||
|
||||
@@ -359,13 +359,6 @@ export function matchAllowlist(
|
||||
return null;
|
||||
}
|
||||
const resolvedPath = resolution.resolvedPath;
|
||||
// argPattern matching is currently Windows-only. On other platforms every
|
||||
// path-matched entry is treated as a match regardless of argPattern, which
|
||||
// preserves the pre-existing behaviour.
|
||||
// Use the caller-supplied target platform rather than process.platform so that
|
||||
// a Linux gateway evaluating a Windows node command applies argPattern correctly.
|
||||
const effectivePlatform = platform ?? process.platform;
|
||||
const useArgPattern = normalizeLowercaseStringOrEmpty(effectivePlatform).startsWith("win");
|
||||
let pathOnlyMatch: ExecAllowlistEntry | null = null;
|
||||
for (const entry of entries) {
|
||||
const pattern = entry.pattern?.trim();
|
||||
@@ -378,10 +371,6 @@ export function matchAllowlist(
|
||||
if (!patternMatches) {
|
||||
continue;
|
||||
}
|
||||
if (!useArgPattern) {
|
||||
// Non-Windows: first path match wins (legacy behaviour).
|
||||
return entry;
|
||||
}
|
||||
if (!entry.argPattern) {
|
||||
if (!pathOnlyMatch) {
|
||||
pathOnlyMatch = entry;
|
||||
|
||||
Reference in New Issue
Block a user