fix(security): avoid prototype-chain account path checks (#34982)

Merged via squash.

Prepared head SHA: f89cc6a649959997fe1dec1e1c1bff9a61b2de98
Co-authored-by: HOYALIM <166576253+HOYALIM@users.noreply.github.com>
Co-authored-by: dvrshil <81693876+dvrshil@users.noreply.github.com>
Reviewed-by: @dvrshil
This commit is contained in:
Ho Lim
2026-03-04 17:38:09 -08:00
committed by GitHub
parent 809f9513ac
commit da0e245db6
4 changed files with 48 additions and 2 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Security/audit account handling: avoid prototype-chain account IDs in audit validation by using own-property checks for `accounts`. (#34982) Thanks @HOYALIM.
- Agents/session usage tracking: preserve accumulated usage metadata on embedded Pi runner error exits so failed turns still update session `totalTokens` from real usage instead of stale prior values. (#34275) thanks @RealKai42.
- Nodes/system.run approval hardening: use explicit argv-mutation signaling when regenerating prepared `rawCommand`, and cover the `system.run.prepare -> system.run` handoff so direct PATH-based `nodes.run` commands no longer fail with `rawCommand does not match command`. (#33137) thanks @Sid-Qin.
- Models/custom provider headers: propagate `models.providers.<name>.headers` across inline, fallback, and registry-found model resolution so header-authenticated proxies consistently receive configured request headers. (#27490) thanks @Sid-Qin.

View File

@@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
import type { TranslationMap } from "../../ui/src/i18n/lib/types.ts";
import {
DEFAULT_LOCALE,
SUPPORTED_LOCALES,
loadLazyLocaleTranslation,
resolveNavigatorLocale,
} from "../../ui/src/i18n/lib/registry.ts";
import type { TranslationMap } from "../../ui/src/i18n/lib/types.ts";
function getNestedTranslation(map: TranslationMap | null, ...path: string[]): string | undefined {
let value: string | TranslationMap | undefined = map ?? undefined;

View File

@@ -108,7 +108,7 @@ function hasExplicitProviderAccountConfig(
if (!accounts || typeof accounts !== "object") {
return false;
}
return accountId in accounts;
return Object.hasOwn(accounts, accountId);
}
export async function collectChannelSecurityFindings(params: {

View File

@@ -1998,6 +1998,51 @@ description: test skill
});
});
it("does not treat prototype properties as explicit Discord account config paths", async () => {
await withChannelSecurityStateDir(async () => {
const cfg: OpenClawConfig = {
channels: {
discord: {
enabled: true,
token: "t",
dangerouslyAllowNameMatching: true,
allowFrom: ["Alice#1234"],
accounts: {},
},
},
};
const pluginWithProtoDefaultAccount: ChannelPlugin = {
...discordPlugin,
config: {
...discordPlugin.config,
listAccountIds: () => [],
defaultAccountId: () => "toString",
},
};
const res = await runSecurityAudit({
config: cfg,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [pluginWithProtoDefaultAccount],
});
const dangerousMatchingFinding = res.findings.find(
(entry) => entry.checkId === "channels.discord.allowFrom.dangerous_name_matching_enabled",
);
expect(dangerousMatchingFinding).toBeDefined();
expect(dangerousMatchingFinding?.title).not.toContain("(account: toString)");
const nameBasedFinding = res.findings.find(
(entry) => entry.checkId === "channels.discord.allowFrom.name_based_entries",
);
expect(nameBasedFinding).toBeDefined();
expect(nameBasedFinding?.detail).toContain("channels.discord.allowFrom:Alice#1234");
expect(nameBasedFinding?.detail).not.toContain("channels.discord.accounts.toString");
});
});
it("audits name-based allowlists on non-default Discord accounts", async () => {
await withChannelSecurityStateDir(async () => {
const cfg: OpenClawConfig = {