diff --git a/package.json b/package.json index f784c2143b9..be9f77b1df4 100644 --- a/package.json +++ b/package.json @@ -1433,7 +1433,6 @@ "lint:swift": "swiftlint lint --config config/swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)", "lint:tmp:channel-agnostic-boundaries": "node scripts/check-channel-agnostic-boundaries.mjs", "lint:tmp:dynamic-import-warts": "node scripts/check-dynamic-import-warts.mjs", - "lint:tmp:managed-proxy-runtime-mutation": "node scripts/check-managed-proxy-runtime-mutation.mjs", "lint:tmp:no-random-messaging": "node scripts/check-no-random-messaging-tmp.mjs", "lint:tmp:no-raw-channel-fetch": "node scripts/check-no-raw-channel-fetch.mjs", "lint:tmp:no-raw-http2-imports": "node scripts/check-no-raw-http2-imports.mjs", diff --git a/scripts/check-managed-proxy-runtime-mutation.mjs b/scripts/check-managed-proxy-runtime-mutation.mjs deleted file mode 100644 index 0fa91340244..00000000000 --- a/scripts/check-managed-proxy-runtime-mutation.mjs +++ /dev/null @@ -1,504 +0,0 @@ -#!/usr/bin/env node - -import ts from "typescript"; -import { runCallsiteGuard } from "./lib/callsite-guard.mjs"; -import { runAsScript, toLine, unwrapExpression } from "./lib/ts-guard-utils.mjs"; - -const sourceRoots = ["src", "extensions"]; - -const allowedManagedProxyRuntimeMutationScopes = new Set([ - // Canonical managed proxy lifecycle owns process proxy env/global-agent mutation. - "src/infra/net/proxy/proxy-lifecycle.ts#applyProxyEnv", - "src/infra/net/proxy/proxy-lifecycle.ts#restoreProxyEnv", - "src/infra/net/proxy/proxy-lifecycle.ts#restoreGlobalAgentRuntime", - "src/infra/net/proxy/proxy-lifecycle.ts#restoreNodeHttpStack", - "src/infra/net/proxy/proxy-lifecycle.ts#bootstrapNodeHttpStack", - "src/infra/net/proxy/proxy-lifecycle.ts#writeGlobalAgentNoProxy", - "src/infra/net/proxy/proxy-lifecycle.ts#disableGlobalAgentProxyForIpv6GatewayLoopback", - - // Browser CDP loopback helper leases NO_PROXY only for localhost/loopback CDP URLs. - "extensions/browser/src/browser/cdp-proxy-bypass.ts#NoProxyLeaseManager.acquire", - "extensions/browser/src/browser/cdp-proxy-bypass.ts#NoProxyLeaseManager.release", -]); - -const forbiddenEnvKeys = new Set([ - "HTTP_PROXY", - "HTTPS_PROXY", - "http_proxy", - "https_proxy", - "NO_PROXY", - "no_proxy", - "GLOBAL_AGENT_HTTP_PROXY", - "GLOBAL_AGENT_HTTPS_PROXY", - "GLOBAL_AGENT_NO_PROXY", - "GLOBAL_AGENT_FORCE_GLOBAL_AGENT", - "OPENCLAW_PROXY_ACTIVE", - "OPENCLAW_PROXY_LOOPBACK_MODE", -]); - -const forbiddenGlobalAgentKeys = new Set(["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"]); - -function stringLiteralText(node) { - return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : null; -} - -function propertyNameText(name) { - if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { - return name.text; - } - return null; -} - -function qualifiedScopeName(name, classScopes) { - const classScope = classScopes.at(-1); - return classScope ? `${classScope}.${name}` : name; -} - -function functionExpressionScopeName(node, classScopes) { - const parent = node.parent; - if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) { - return parent.name.text; - } - if (ts.isPropertyAssignment(parent)) { - const name = propertyNameText(parent.name); - return name ? qualifiedScopeName(name, classScopes) : null; - } - if (ts.isPropertyDeclaration(parent) && parent.name) { - const name = propertyNameText(parent.name); - return name ? qualifiedScopeName(name, classScopes) : null; - } - return null; -} - -function scopeNameForNode(node, classScopes) { - if (ts.isFunctionDeclaration(node) && node.name) { - return node.name.text; - } - if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) { - return functionExpressionScopeName(node, classScopes); - } - if ( - ts.isMethodDeclaration(node) || - ts.isGetAccessorDeclaration(node) || - ts.isSetAccessorDeclaration(node) - ) { - const name = propertyNameText(node.name); - return name ? qualifiedScopeName(name, classScopes) : null; - } - if (ts.isConstructorDeclaration(node)) { - return qualifiedScopeName("constructor", classScopes); - } - return null; -} - -function isGlobalIdentifier(node, context = { globalAliases: new Set() }) { - const unwrapped = unwrapExpression(node); - return ( - ts.isIdentifier(unwrapped) && - (unwrapped.text === "global" || - unwrapped.text === "globalThis" || - context.globalAliases.has(unwrapped.text)) - ); -} - -function processEnvExpression(expression, context = { envAliases: new Set() }) { - const unwrapped = unwrapExpression(expression); - if (ts.isIdentifier(unwrapped) && context.envAliases.has(unwrapped.text)) { - return unwrapped; - } - if (ts.isPropertyAccessExpression(unwrapped) && unwrapped.name.text === "env") { - const base = unwrapExpression(unwrapped.expression); - return ts.isIdentifier(base) && base.text === "process" ? unwrapped : null; - } - if (ts.isElementAccessExpression(unwrapped)) { - const key = stringLiteralText(unwrapExpression(unwrapped.argumentExpression)); - if (key !== "env") { - return null; - } - const base = unwrapExpression(unwrapped.expression); - return ts.isIdentifier(base) && base.text === "process" ? unwrapped : null; - } - return null; -} - -function collectStringConstants(sourceFile) { - const constants = new Map(); - const visit = (node) => { - if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) { - const literal = stringLiteralText(unwrapExpression(node.initializer)); - if (literal) { - constants.set(node.name.text, literal); - } - } - ts.forEachChild(node, visit); - }; - visit(sourceFile); - return constants; -} - -function collectStringArrays(sourceFile) { - const arrays = new Map(); - for (let pass = 0; pass < 4; pass += 1) { - const visit = (node) => { - if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) { - const initializer = unwrapExpression(node.initializer); - if (ts.isArrayLiteralExpression(initializer)) { - const values = []; - let complete = true; - for (const element of initializer.elements) { - const unwrapped = unwrapExpression(element); - if ( - ts.isSpreadElement(unwrapped) && - ts.isIdentifier(unwrapExpression(unwrapped.expression)) - ) { - const nested = arrays.get(unwrapExpression(unwrapped.expression).text); - if (nested) { - values.push(...nested); - } else { - complete = false; - } - continue; - } - const literal = stringLiteralText(unwrapped); - if (literal) { - values.push(literal); - } else { - complete = false; - } - } - if (complete) { - const previous = arrays.get(node.name.text); - const sameValues = previous && previous.join("\0") === values.join("\0"); - if (!sameValues) { - arrays.set(node.name.text, values); - } - } - } - } - ts.forEachChild(node, visit); - }; - visit(sourceFile); - } - return arrays; -} - -function collectForbiddenKeyArrays(sourceFile) { - const stringArrays = collectStringArrays(sourceFile); - const arrays = new Set(); - for (const [name, values] of stringArrays) { - if (values.some((key) => forbiddenEnvKeys.has(key))) { - arrays.add(name); - } - } - return arrays; -} - -function collectForbiddenKeyVariables(sourceFile, forbiddenKeyArrays) { - const variables = new Set(); - const visit = (node) => { - if (ts.isForOfStatement(node)) { - const expression = unwrapExpression(node.expression); - if (ts.isIdentifier(expression) && forbiddenKeyArrays.has(expression.text)) { - const initializer = node.initializer; - if (ts.isVariableDeclarationList(initializer)) { - for (const declaration of initializer.declarations) { - if (ts.isIdentifier(declaration.name)) { - variables.add(declaration.name.text); - } - } - } else if (ts.isIdentifier(initializer)) { - variables.add(initializer.text); - } - } - } - ts.forEachChild(node, visit); - }; - visit(sourceFile); - return variables; -} - -function envKeyExpressionIsForbidden(argumentExpression, context) { - const keyExpression = unwrapExpression(argumentExpression); - const literalKey = stringLiteralText(keyExpression); - if (literalKey) { - return forbiddenEnvKeys.has(literalKey); - } - if (!ts.isIdentifier(keyExpression)) { - return false; - } - if (context.forbiddenKeyVariables.has(keyExpression.text)) { - return true; - } - const constant = context.stringConstants.get(keyExpression.text); - return constant ? forbiddenEnvKeys.has(constant) : false; -} - -function envMutationTarget(expression, context) { - const unwrapped = unwrapExpression(expression); - if ( - ts.isPropertyAccessExpression(unwrapped) && - processEnvExpression(unwrapped.expression, context) - ) { - return forbiddenEnvKeys.has(unwrapped.name.text) ? unwrapped : null; - } - if ( - ts.isElementAccessExpression(unwrapped) && - processEnvExpression(unwrapped.expression, context) - ) { - return envKeyExpressionIsForbidden(unwrapped.argumentExpression, context) ? unwrapped : null; - } - return null; -} - -function globalAgentExpression( - expression, - context = { globalAgentAliases: new Set(), globalAliases: new Set() }, -) { - const unwrapped = unwrapExpression(expression); - if (ts.isIdentifier(unwrapped) && context.globalAgentAliases.has(unwrapped.text)) { - return unwrapped; - } - if (ts.isPropertyAccessExpression(unwrapped)) { - const receiver = unwrapExpression(unwrapped.expression); - if (unwrapped.name.text === "GLOBAL_AGENT" && isGlobalIdentifier(receiver, context)) { - return unwrapped; - } - } - if (ts.isElementAccessExpression(unwrapped)) { - const receiver = unwrapExpression(unwrapped.expression); - const key = stringLiteralText(unwrapExpression(unwrapped.argumentExpression)); - if (key === "GLOBAL_AGENT" && isGlobalIdentifier(receiver, context)) { - return unwrapped; - } - } - return null; -} - -function collectEnvAliases(sourceFile) { - const aliases = new Set(); - const emptyContext = { envAliases: new Set() }; - const visit = (node) => { - if (ts.isVariableDeclaration(node) && node.initializer) { - if (ts.isIdentifier(node.name) && processEnvExpression(node.initializer, emptyContext)) { - aliases.add(node.name.text); - } - if (ts.isObjectBindingPattern(node.name)) { - const initializer = unwrapExpression(node.initializer); - if (ts.isIdentifier(initializer) && initializer.text === "process") { - for (const element of node.name.elements) { - if (!ts.isIdentifier(element.name)) { - continue; - } - const importedName = - element.propertyName && ts.isIdentifier(element.propertyName) - ? element.propertyName.text - : element.name.text; - if (importedName === "env") { - aliases.add(element.name.text); - } - } - } - } - } - ts.forEachChild(node, visit); - }; - visit(sourceFile); - return aliases; -} - -function collectGlobalAliases(sourceFile) { - const aliases = new Set(); - const visit = (node) => { - if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) { - if (isGlobalIdentifier(node.initializer, { globalAliases: new Set() })) { - aliases.add(node.name.text); - } - } - ts.forEachChild(node, visit); - }; - visit(sourceFile); - return aliases; -} - -function collectGlobalAgentAliases(sourceFile, globalAliases = collectGlobalAliases(sourceFile)) { - const aliases = new Set(); - const emptyContext = { globalAgentAliases: new Set(), globalAliases }; - const visit = (node) => { - if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) { - if (globalAgentExpression(node.initializer, emptyContext)) { - aliases.add(node.name.text); - } - } - ts.forEachChild(node, visit); - }; - visit(sourceFile); - return aliases; -} - -function globalAgentMutationTarget(expression, context) { - const unwrapped = unwrapExpression(expression); - if (globalAgentExpression(unwrapped, context)) { - return unwrapped; - } - if ( - ts.isPropertyAccessExpression(unwrapped) && - globalAgentExpression(unwrapped.expression, context) - ) { - return forbiddenGlobalAgentKeys.has(unwrapped.name.text) ? unwrapped : null; - } - if ( - ts.isElementAccessExpression(unwrapped) && - globalAgentExpression(unwrapped.expression, context) - ) { - const key = stringLiteralText(unwrapExpression(unwrapped.argumentExpression)); - return key && forbiddenGlobalAgentKeys.has(key) ? unwrapped : null; - } - return null; -} - -function mutationTarget(expression, context) { - return envMutationTarget(expression, context) ?? globalAgentMutationTarget(expression, context); -} - -function deleteTarget(expression, context) { - const unwrapped = unwrapExpression(expression); - return ts.isDeleteExpression(unwrapped) ? mutationTarget(unwrapped.expression, context) : null; -} - -function assignmentTarget(expression, context) { - const unwrapped = unwrapExpression(expression); - if (ts.isBinaryExpression(unwrapped) && ts.isAssignmentOperator(unwrapped.operatorToken.kind)) { - return mutationTarget(unwrapped.left, context); - } - return null; -} - -function mutatingCallTarget(expression, context) { - const unwrapped = unwrapExpression(expression); - if (!ts.isCallExpression(unwrapped)) { - return null; - } - const callee = unwrapExpression(unwrapped.expression); - if (!ts.isPropertyAccessExpression(callee)) { - return null; - } - const method = callee.name.text; - if (method !== "defineProperty" && method !== "assign") { - return null; - } - const receiver = unwrapExpression(callee.expression); - if (!ts.isIdentifier(receiver) || receiver.text !== "Object") { - return null; - } - const first = unwrapped.arguments[0] ? unwrapExpression(unwrapped.arguments[0]) : null; - if (!first) { - return null; - } - if (method === "assign") { - return globalAgentExpression(first, context) ?? processEnvExpression(first, context); - } - const rawKeyArg = unwrapped.arguments[1] ? unwrapExpression(unwrapped.arguments[1]) : null; - const literalKeyArg = rawKeyArg ? stringLiteralText(rawKeyArg) : null; - const keyArg = - literalKeyArg ?? - (rawKeyArg && ts.isIdentifier(rawKeyArg) ? context.stringConstants.get(rawKeyArg.text) : null); - if (keyArg && processEnvExpression(first, context)) { - return forbiddenEnvKeys.has(keyArg) ? first : null; - } - if (keyArg && globalAgentExpression(first, context)) { - return forbiddenGlobalAgentKeys.has(keyArg) ? first : null; - } - return null; -} - -export function findManagedProxyRuntimeMutations(content, fileName = "source.ts") { - const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true); - const globalAliases = collectGlobalAliases(sourceFile); - const context = { - forbiddenKeyVariables: collectForbiddenKeyVariables( - sourceFile, - collectForbiddenKeyArrays(sourceFile), - ), - globalAgentAliases: collectGlobalAgentAliases(sourceFile, globalAliases), - globalAliases, - envAliases: collectEnvAliases(sourceFile), - stringConstants: collectStringConstants(sourceFile), - }; - const mutations = []; - const classScopes = []; - const scopeStack = []; - const visit = (node) => { - let pushedClass = false; - let pushedScope = false; - if (ts.isClassDeclaration(node) && node.name) { - classScopes.push(node.name.text); - pushedClass = true; - } - const scopeName = scopeNameForNode(node, classScopes); - if (scopeName) { - scopeStack.push(scopeName); - pushedScope = true; - } - - const match = - assignmentTarget(node, context) ?? - deleteTarget(node, context) ?? - mutatingCallTarget(node, context); - if (match) { - mutations.push({ - line: toLine(sourceFile, match), - scope: scopeStack.at(-1) ?? null, - }); - } - ts.forEachChild(node, visit); - - if (pushedScope) { - scopeStack.pop(); - } - if (pushedClass) { - classScopes.pop(); - } - }; - visit(sourceFile); - return mutations; -} - -export function findManagedProxyRuntimeMutationLines(content, fileName = "source.ts") { - return findManagedProxyRuntimeMutations(content, fileName).map((mutation) => mutation.line); -} - -export function isAllowedManagedProxyRuntimeMutation(violation) { - if (!violation.scope) { - return false; - } - return allowedManagedProxyRuntimeMutationScopes.has( - `${violation.relativePath}#${violation.scope}`, - ); -} - -function formatManagedProxyRuntimeMutationCallsite(violation) { - const scope = violation.scope ? ` (${violation.scope})` : ""; - return `${violation.relativePath}:${violation.line}${scope}`; -} - -export async function main() { - await runCallsiteGuard({ - importMetaUrl: import.meta.url, - sourceRoots, - extraTestSuffixes: [ - ".browser.test.ts", - ".node.test.ts", - ".live.test.ts", - ".e2e.test.ts", - ".integration.test.ts", - ], - findCallViolations: findManagedProxyRuntimeMutations, - allowViolation: isAllowedManagedProxyRuntimeMutation, - formatViolation: formatManagedProxyRuntimeMutationCallsite, - header: "Found unmanaged managed-proxy runtime mutation:", - footer: - "Only proxy lifecycle code may mutate GLOBAL_AGENT or proxy-related process.env runtime state.", - }); -} - -runAsScript(import.meta.url, main); diff --git a/scripts/lib/callsite-guard.mjs b/scripts/lib/callsite-guard.mjs index a4ff9d205c0..94715e9cb9b 100644 --- a/scripts/lib/callsite-guard.mjs +++ b/scripts/lib/callsite-guard.mjs @@ -6,19 +6,6 @@ import { resolveSourceRoots, } from "./ts-guard-utils.mjs"; -function normalizeViolation(rawViolation, relPath) { - if (typeof rawViolation === "number") { - return { - line: rawViolation, - callsite: `${relPath}:${rawViolation}`, - }; - } - return { - ...rawViolation, - callsite: rawViolation.callsite ?? `${relPath}:${rawViolation.line}`, - }; -} - export async function runCallsiteGuard(params) { const repoRoot = resolveRepoRoot(params.importMetaUrl); const sourceRoots = resolveSourceRoots(repoRoot, params.sourceRoots); @@ -33,30 +20,12 @@ export async function runCallsiteGuard(params) { continue; } const content = await fs.readFile(filePath, "utf8"); - const rawViolations = params.findCallViolations - ? params.findCallViolations(content, filePath) - : params.findCallLines(content, filePath); - for (const rawViolation of rawViolations) { - const violation = normalizeViolation(rawViolation, relPath); - if ( - params.allowViolation?.({ - ...violation, - relativePath: relPath, - filePath, - }) ?? - params.allowCallsite?.(violation.callsite, violation) - ) { + for (const line of params.findCallLines(content, filePath)) { + const callsite = `${relPath}:${line}`; + if (params.allowCallsite?.(callsite)) { continue; } - violations.push( - params.formatViolation - ? params.formatViolation({ - ...violation, - relativePath: relPath, - filePath, - }) - : violation.callsite, - ); + violations.push(callsite); } } diff --git a/scripts/run-additional-boundary-checks.mjs b/scripts/run-additional-boundary-checks.mjs index d9264b69b0a..43e0732e8fd 100644 --- a/scripts/run-additional-boundary-checks.mjs +++ b/scripts/run-additional-boundary-checks.mjs @@ -10,11 +10,6 @@ export const BOUNDARY_CHECKS = [ ["lint:tmp:tsgo-core-boundary", "pnpm", ["run", "lint:tmp:tsgo-core-boundary"]], ["lint:tmp:no-raw-channel-fetch", "pnpm", ["run", "lint:tmp:no-raw-channel-fetch"]], ["lint:tmp:no-raw-http2-imports", "pnpm", ["run", "lint:tmp:no-raw-http2-imports"]], - [ - "lint:tmp:managed-proxy-runtime-mutation", - "pnpm", - ["run", "lint:tmp:managed-proxy-runtime-mutation"], - ], ["lint:tmp:raw-socket-classification", "pnpm", ["run", "lint:tmp:raw-socket-classification"]], ["lint:agent:ingress-owner", "pnpm", ["run", "lint:agent:ingress-owner"]], [ diff --git a/security/opengrep/precise.yml b/security/opengrep/precise.yml index 99f874c9053..2eb13d9f4f4 100644 --- a/security/opengrep/precise.yml +++ b/security/opengrep/precise.yml @@ -4,8 +4,8 @@ # DO NOT EDIT BY HAND. Re-run the compile script after editing source rules. # # Source rules dir: security/opengrep/rules/openclaw-policy -# Generated at : 2026-04-30T09:09:41.198Z -# Rule count : 148 +# Generated at : 2026-05-07T04:40:02.803Z +# Rule count : 154 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. @@ -5010,3 +5010,513 @@ rules: - "**/*.test.mjs" patterns: - pattern: http2.connect(...) + - id: openclaw-policy-managed-proxy-runtime-mutation.managed-proxy-process-env-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate proxy-related process.env runtime state. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - CWE-441 + category: security + confidence: HIGH + detector-bucket: precise + source-rule-id: managed-proxy-process-env-mutation + source-file: security/opengrep/rules/openclaw-policy/managed-proxy-runtime-mutation.yml + paths: + include: + - src/**/*.ts + - src/**/*.mts + - src/**/*.js + - src/**/*.mjs + - extensions/**/*.ts + - extensions/**/*.mts + - extensions/**/*.js + - extensions/**/*.mjs + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - pattern: process.env.$KEY = ... + - pattern: process.env[$KEY] = ... + - pattern: delete process.env.$KEY + - pattern: delete process.env[$KEY] + - pattern: Object.defineProperty(process.env, $KEY, ...) + - pattern: | + Object.assign(process.env, { $KEY: ... }) + - metavariable-regex: + metavariable: $KEY + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|NO_PROXY|no_proxy|GLOBAL_AGENT_HTTP_PROXY|GLOBAL_AGENT_HTTPS_PROXY|GLOBAL_AGENT_NO_PROXY|GLOBAL_AGENT_FORCE_GLOBAL_AGENT|OPENCLAW_PROXY_ACTIVE|OPENCLAW_PROXY_LOOPBACK_MODE)["']?$ + - pattern-not-inside: | + function applyProxyEnv(...) { + ... + } + - pattern-not-inside: | + function restoreProxyEnv(...) { + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + acquire(...) { + ... + } + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + release(...) { + ... + } + ... + } + - id: openclaw-policy-managed-proxy-runtime-mutation.managed-proxy-process-env-alias-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate proxy-related process.env aliases. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - CWE-441 + category: security + confidence: HIGH + detector-bucket: precise + source-rule-id: managed-proxy-process-env-alias-mutation + source-file: security/opengrep/rules/openclaw-policy/managed-proxy-runtime-mutation.yml + paths: + include: + - src/**/*.ts + - src/**/*.mts + - src/**/*.js + - src/**/*.mjs + - extensions/**/*.ts + - extensions/**/*.mts + - extensions/**/*.js + - extensions/**/*.mjs + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + const $ENV = process.env; + ... + - pattern-either: + - pattern: $ENV.$KEY = ... + - pattern: $ENV[$KEY] = ... + - pattern: delete $ENV.$KEY + - pattern: delete $ENV[$KEY] + - pattern: Object.defineProperty($ENV, $KEY, ...) + - pattern: | + Object.assign($ENV, { $KEY: ... }) + - patterns: + - pattern-inside: | + const { env } = process; + ... + - pattern-either: + - pattern: env.$KEY = ... + - pattern: env[$KEY] = ... + - pattern: delete env.$KEY + - pattern: delete env[$KEY] + - pattern: Object.defineProperty(env, $KEY, ...) + - pattern: | + Object.assign(env, { $KEY: ... }) + - metavariable-regex: + metavariable: $KEY + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|NO_PROXY|no_proxy|GLOBAL_AGENT_HTTP_PROXY|GLOBAL_AGENT_HTTPS_PROXY|GLOBAL_AGENT_NO_PROXY|GLOBAL_AGENT_FORCE_GLOBAL_AGENT|OPENCLAW_PROXY_ACTIVE|OPENCLAW_PROXY_LOOPBACK_MODE)["']?$ + - pattern-not-inside: | + function applyProxyEnv(...) { + ... + } + - pattern-not-inside: | + function restoreProxyEnv(...) { + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + acquire(...) { + ... + } + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + release(...) { + ... + } + ... + } + - id: openclaw-policy-managed-proxy-runtime-mutation.managed-proxy-process-env-dynamic-key-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate process.env through proxy-related dynamic keys. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - CWE-441 + category: security + confidence: HIGH + detector-bucket: precise + source-rule-id: managed-proxy-process-env-dynamic-key-mutation + source-file: security/opengrep/rules/openclaw-policy/managed-proxy-runtime-mutation.yml + paths: + include: + - src/**/*.ts + - src/**/*.mts + - src/**/*.js + - src/**/*.mjs + - extensions/**/*.ts + - extensions/**/*.mts + - extensions/**/*.js + - extensions/**/*.mjs + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + const $KEYS = [..., $FORBIDDEN, ...]; + ... + for (const $KEY of $KEYS) { + ... + } + - pattern-either: + - pattern: process.env[$KEY] = ... + - pattern: delete process.env[$KEY] + - patterns: + - pattern-inside: | + const $SOURCE_KEYS = [..., $FORBIDDEN, ...]; + ... + const $KEYS = [..., ...$SOURCE_KEYS, ...]; + ... + for (const $KEY of $KEYS) { + ... + } + - pattern-either: + - pattern: process.env[$KEY] = ... + - pattern: delete process.env[$KEY] + - patterns: + - pattern-inside: | + const $ENV = process.env; + ... + const $KEYS = [..., $FORBIDDEN, ...]; + ... + for (const $KEY of $KEYS) { + ... + } + - pattern-either: + - pattern: $ENV[$KEY] = ... + - pattern: delete $ENV[$KEY] + - metavariable-regex: + metavariable: $FORBIDDEN + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|NO_PROXY|no_proxy|GLOBAL_AGENT_HTTP_PROXY|GLOBAL_AGENT_HTTPS_PROXY|GLOBAL_AGENT_NO_PROXY|GLOBAL_AGENT_FORCE_GLOBAL_AGENT|OPENCLAW_PROXY_ACTIVE|OPENCLAW_PROXY_LOOPBACK_MODE)["']?$ + - pattern-not-inside: | + function applyProxyEnv(...) { + ... + } + - pattern-not-inside: | + function restoreProxyEnv(...) { + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + acquire(...) { + ... + } + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + release(...) { + ... + } + ... + } + - id: openclaw-policy-managed-proxy-runtime-mutation.managed-proxy-global-agent-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate GLOBAL_AGENT proxy runtime state. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - CWE-441 + category: security + confidence: HIGH + detector-bucket: precise + source-rule-id: managed-proxy-global-agent-mutation + source-file: security/opengrep/rules/openclaw-policy/managed-proxy-runtime-mutation.yml + paths: + include: + - src/**/*.ts + - src/**/*.mts + - src/**/*.js + - src/**/*.mjs + - extensions/**/*.ts + - extensions/**/*.mts + - extensions/**/*.js + - extensions/**/*.mjs + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - pattern: global.GLOBAL_AGENT = ... + - pattern: globalThis.GLOBAL_AGENT = ... + - pattern: global["GLOBAL_AGENT"] = ... + - pattern: globalThis["GLOBAL_AGENT"] = ... + - pattern: global.GLOBAL_AGENT.$KEY = ... + - pattern: global.GLOBAL_AGENT[$KEY] = ... + - pattern: globalThis.GLOBAL_AGENT.$KEY = ... + - pattern: globalThis.GLOBAL_AGENT[$KEY] = ... + - pattern: global["GLOBAL_AGENT"][$KEY] = ... + - pattern: globalThis["GLOBAL_AGENT"][$KEY] = ... + - pattern: delete global.GLOBAL_AGENT + - pattern: delete globalThis.GLOBAL_AGENT + - pattern: delete global["GLOBAL_AGENT"] + - pattern: delete globalThis["GLOBAL_AGENT"] + - pattern: delete global.GLOBAL_AGENT.$KEY + - pattern: delete global.GLOBAL_AGENT[$KEY] + - pattern: delete globalThis.GLOBAL_AGENT.$KEY + - pattern: delete globalThis.GLOBAL_AGENT[$KEY] + - pattern: Object.defineProperty(global.GLOBAL_AGENT, $KEY, ...) + - pattern: Object.defineProperty(globalThis.GLOBAL_AGENT, $KEY, ...) + - pattern: | + Object.assign(global.GLOBAL_AGENT, { $KEY: ... }) + - pattern: | + Object.assign(globalThis.GLOBAL_AGENT, { $KEY: ... }) + - metavariable-regex: + metavariable: $KEY + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|NO_PROXY)["']?$ + - pattern-not-inside: | + function restoreGlobalAgentRuntime(...) { + ... + } + - pattern-not-inside: | + function restoreNodeHttpStack(...) { + ... + } + - pattern-not-inside: | + function bootstrapNodeHttpStack(...) { + ... + } + - pattern-not-inside: | + function writeGlobalAgentNoProxy(...) { + ... + } + - pattern-not-inside: | + function disableGlobalAgentProxyForIpv6GatewayLoopback(...) { + ... + } + - id: openclaw-policy-managed-proxy-runtime-mutation.managed-proxy-global-agent-object-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may replace or delete GLOBAL_AGENT runtime state. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - CWE-441 + category: security + confidence: HIGH + detector-bucket: precise + source-rule-id: managed-proxy-global-agent-object-mutation + source-file: security/opengrep/rules/openclaw-policy/managed-proxy-runtime-mutation.yml + paths: + include: + - src/**/*.ts + - src/**/*.mts + - src/**/*.js + - src/**/*.mjs + - extensions/**/*.ts + - extensions/**/*.mts + - extensions/**/*.js + - extensions/**/*.mjs + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - pattern: global.GLOBAL_AGENT = ... + - pattern: globalThis.GLOBAL_AGENT = ... + - pattern: global["GLOBAL_AGENT"] = ... + - pattern: globalThis["GLOBAL_AGENT"] = ... + - pattern: delete global.GLOBAL_AGENT + - pattern: delete globalThis.GLOBAL_AGENT + - pattern: delete global["GLOBAL_AGENT"] + - pattern: delete globalThis["GLOBAL_AGENT"] + - patterns: + - pattern-inside: | + const $GLOBAL = global; + ... + - pattern-either: + - pattern: $GLOBAL.GLOBAL_AGENT = ... + - pattern: $GLOBAL["GLOBAL_AGENT"] = ... + - pattern: delete $GLOBAL.GLOBAL_AGENT + - pattern: delete $GLOBAL["GLOBAL_AGENT"] + - patterns: + - pattern-inside: | + const $GLOBAL = global as $TYPE; + ... + - pattern-either: + - pattern: $GLOBAL.GLOBAL_AGENT = ... + - pattern: $GLOBAL["GLOBAL_AGENT"] = ... + - pattern: delete $GLOBAL.GLOBAL_AGENT + - pattern: delete $GLOBAL["GLOBAL_AGENT"] + - pattern-not-inside: | + function restoreNodeHttpStack(...) { + ... + } + - id: openclaw-policy-managed-proxy-runtime-mutation.managed-proxy-global-agent-alias-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate GLOBAL_AGENT aliases. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - CWE-441 + category: security + confidence: HIGH + detector-bucket: precise + source-rule-id: managed-proxy-global-agent-alias-mutation + source-file: security/opengrep/rules/openclaw-policy/managed-proxy-runtime-mutation.yml + paths: + include: + - src/**/*.ts + - src/**/*.mts + - src/**/*.js + - src/**/*.mjs + - extensions/**/*.ts + - extensions/**/*.mts + - extensions/**/*.js + - extensions/**/*.mjs + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + const $AGENT = global.GLOBAL_AGENT; + ... + - pattern-either: + - pattern: $AGENT.$KEY = ... + - pattern: $AGENT[$KEY] = ... + - pattern: delete $AGENT.$KEY + - pattern: delete $AGENT[$KEY] + - pattern: Object.defineProperty($AGENT, $KEY, ...) + - pattern: | + Object.assign($AGENT, { $KEY: ... }) + - patterns: + - pattern-inside: | + const $AGENT = global["GLOBAL_AGENT"]; + ... + - pattern-either: + - pattern: $AGENT.$KEY = ... + - pattern: $AGENT[$KEY] = ... + - pattern: delete $AGENT.$KEY + - pattern: delete $AGENT[$KEY] + - pattern: Object.defineProperty($AGENT, $KEY, ...) + - pattern: | + Object.assign($AGENT, { $KEY: ... }) + - patterns: + - pattern-inside: | + const $AGENT = (global as $TYPE)["GLOBAL_AGENT"] as $AGENT_TYPE; + ... + - pattern-either: + - pattern: $AGENT.$KEY = ... + - pattern: $AGENT[$KEY] = ... + - pattern: delete $AGENT.$KEY + - pattern: delete $AGENT[$KEY] + - pattern: Object.defineProperty($AGENT, $KEY, ...) + - pattern: | + Object.assign($AGENT, { $KEY: ... }) + - patterns: + - pattern-inside: | + const $GLOBAL = global; + ... + - pattern-either: + - pattern: $GLOBAL.GLOBAL_AGENT = ... + - pattern: $GLOBAL["GLOBAL_AGENT"] = ... + - pattern: $GLOBAL.GLOBAL_AGENT.$KEY = ... + - pattern: $GLOBAL.GLOBAL_AGENT[$KEY] = ... + - pattern: $GLOBAL["GLOBAL_AGENT"][$KEY] = ... + - pattern: delete $GLOBAL.GLOBAL_AGENT + - pattern: delete $GLOBAL["GLOBAL_AGENT"] + - pattern: delete $GLOBAL.GLOBAL_AGENT.$KEY + - pattern: delete $GLOBAL.GLOBAL_AGENT[$KEY] + - pattern: delete $GLOBAL["GLOBAL_AGENT"][$KEY] + - patterns: + - pattern-inside: | + const $GLOBAL = global as $TYPE; + ... + - pattern-either: + - pattern: $GLOBAL.GLOBAL_AGENT = ... + - pattern: $GLOBAL["GLOBAL_AGENT"] = ... + - pattern: $GLOBAL.GLOBAL_AGENT.$KEY = ... + - pattern: $GLOBAL.GLOBAL_AGENT[$KEY] = ... + - pattern: $GLOBAL["GLOBAL_AGENT"][$KEY] = ... + - pattern: delete $GLOBAL.GLOBAL_AGENT + - pattern: delete $GLOBAL["GLOBAL_AGENT"] + - pattern: delete $GLOBAL.GLOBAL_AGENT.$KEY + - pattern: delete $GLOBAL.GLOBAL_AGENT[$KEY] + - pattern: delete $GLOBAL["GLOBAL_AGENT"][$KEY] + - metavariable-regex: + metavariable: $KEY + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|NO_PROXY)["']?$ + - pattern-not-inside: | + function restoreGlobalAgentRuntime(...) { + ... + } + - pattern-not-inside: | + function restoreNodeHttpStack(...) { + ... + } + - pattern-not-inside: | + function bootstrapNodeHttpStack(...) { + ... + } + - pattern-not-inside: | + function writeGlobalAgentNoProxy(...) { + ... + } + - pattern-not-inside: | + function disableGlobalAgentProxyForIpv6GatewayLoopback(...) { + ... + } diff --git a/security/opengrep/rules/openclaw-policy/managed-proxy-runtime-mutation.yml b/security/opengrep/rules/openclaw-policy/managed-proxy-runtime-mutation.yml new file mode 100644 index 00000000000..6ad563a1eb5 --- /dev/null +++ b/security/opengrep/rules/openclaw-policy/managed-proxy-runtime-mutation.yml @@ -0,0 +1,498 @@ +rules: + - id: managed-proxy-process-env-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate proxy-related process.env runtime state. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - "CWE-441" + category: security + confidence: HIGH + paths: + include: + - "src/**/*.ts" + - "src/**/*.mts" + - "src/**/*.js" + - "src/**/*.mjs" + - "extensions/**/*.ts" + - "extensions/**/*.mts" + - "extensions/**/*.js" + - "extensions/**/*.mjs" + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - pattern: process.env.$KEY = ... + - pattern: process.env[$KEY] = ... + - pattern: delete process.env.$KEY + - pattern: delete process.env[$KEY] + - pattern: Object.defineProperty(process.env, $KEY, ...) + - pattern: | + Object.assign(process.env, { $KEY: ... }) + - metavariable-regex: + metavariable: $KEY + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|NO_PROXY|no_proxy|GLOBAL_AGENT_HTTP_PROXY|GLOBAL_AGENT_HTTPS_PROXY|GLOBAL_AGENT_NO_PROXY|GLOBAL_AGENT_FORCE_GLOBAL_AGENT|OPENCLAW_PROXY_ACTIVE|OPENCLAW_PROXY_LOOPBACK_MODE)["']?$ + - pattern-not-inside: | + function applyProxyEnv(...) { + ... + } + - pattern-not-inside: | + function restoreProxyEnv(...) { + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + acquire(...) { + ... + } + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + release(...) { + ... + } + ... + } + + - id: managed-proxy-process-env-alias-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate proxy-related process.env aliases. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - "CWE-441" + category: security + confidence: HIGH + paths: + include: + - "src/**/*.ts" + - "src/**/*.mts" + - "src/**/*.js" + - "src/**/*.mjs" + - "extensions/**/*.ts" + - "extensions/**/*.mts" + - "extensions/**/*.js" + - "extensions/**/*.mjs" + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + const $ENV = process.env; + ... + - pattern-either: + - pattern: $ENV.$KEY = ... + - pattern: $ENV[$KEY] = ... + - pattern: delete $ENV.$KEY + - pattern: delete $ENV[$KEY] + - pattern: Object.defineProperty($ENV, $KEY, ...) + - pattern: | + Object.assign($ENV, { $KEY: ... }) + - patterns: + - pattern-inside: | + const { env } = process; + ... + - pattern-either: + - pattern: env.$KEY = ... + - pattern: env[$KEY] = ... + - pattern: delete env.$KEY + - pattern: delete env[$KEY] + - pattern: Object.defineProperty(env, $KEY, ...) + - pattern: | + Object.assign(env, { $KEY: ... }) + - metavariable-regex: + metavariable: $KEY + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|NO_PROXY|no_proxy|GLOBAL_AGENT_HTTP_PROXY|GLOBAL_AGENT_HTTPS_PROXY|GLOBAL_AGENT_NO_PROXY|GLOBAL_AGENT_FORCE_GLOBAL_AGENT|OPENCLAW_PROXY_ACTIVE|OPENCLAW_PROXY_LOOPBACK_MODE)["']?$ + - pattern-not-inside: | + function applyProxyEnv(...) { + ... + } + - pattern-not-inside: | + function restoreProxyEnv(...) { + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + acquire(...) { + ... + } + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + release(...) { + ... + } + ... + } + + - id: managed-proxy-process-env-dynamic-key-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate process.env through proxy-related dynamic keys. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - "CWE-441" + category: security + confidence: HIGH + paths: + include: + - "src/**/*.ts" + - "src/**/*.mts" + - "src/**/*.js" + - "src/**/*.mjs" + - "extensions/**/*.ts" + - "extensions/**/*.mts" + - "extensions/**/*.js" + - "extensions/**/*.mjs" + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + const $KEYS = [..., $FORBIDDEN, ...]; + ... + for (const $KEY of $KEYS) { + ... + } + - pattern-either: + - pattern: process.env[$KEY] = ... + - pattern: delete process.env[$KEY] + - patterns: + - pattern-inside: | + const $SOURCE_KEYS = [..., $FORBIDDEN, ...]; + ... + const $KEYS = [..., ...$SOURCE_KEYS, ...]; + ... + for (const $KEY of $KEYS) { + ... + } + - pattern-either: + - pattern: process.env[$KEY] = ... + - pattern: delete process.env[$KEY] + - patterns: + - pattern-inside: | + const $ENV = process.env; + ... + const $KEYS = [..., $FORBIDDEN, ...]; + ... + for (const $KEY of $KEYS) { + ... + } + - pattern-either: + - pattern: $ENV[$KEY] = ... + - pattern: delete $ENV[$KEY] + - metavariable-regex: + metavariable: $FORBIDDEN + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|NO_PROXY|no_proxy|GLOBAL_AGENT_HTTP_PROXY|GLOBAL_AGENT_HTTPS_PROXY|GLOBAL_AGENT_NO_PROXY|GLOBAL_AGENT_FORCE_GLOBAL_AGENT|OPENCLAW_PROXY_ACTIVE|OPENCLAW_PROXY_LOOPBACK_MODE)["']?$ + - pattern-not-inside: | + function applyProxyEnv(...) { + ... + } + - pattern-not-inside: | + function restoreProxyEnv(...) { + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + acquire(...) { + ... + } + ... + } + - pattern-not-inside: | + class NoProxyLeaseManager { + ... + release(...) { + ... + } + ... + } + + - id: managed-proxy-global-agent-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate GLOBAL_AGENT proxy runtime state. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - "CWE-441" + category: security + confidence: HIGH + paths: + include: + - "src/**/*.ts" + - "src/**/*.mts" + - "src/**/*.js" + - "src/**/*.mjs" + - "extensions/**/*.ts" + - "extensions/**/*.mts" + - "extensions/**/*.js" + - "extensions/**/*.mjs" + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - pattern: global.GLOBAL_AGENT = ... + - pattern: globalThis.GLOBAL_AGENT = ... + - pattern: global["GLOBAL_AGENT"] = ... + - pattern: globalThis["GLOBAL_AGENT"] = ... + - pattern: global.GLOBAL_AGENT.$KEY = ... + - pattern: global.GLOBAL_AGENT[$KEY] = ... + - pattern: globalThis.GLOBAL_AGENT.$KEY = ... + - pattern: globalThis.GLOBAL_AGENT[$KEY] = ... + - pattern: global["GLOBAL_AGENT"][$KEY] = ... + - pattern: globalThis["GLOBAL_AGENT"][$KEY] = ... + - pattern: delete global.GLOBAL_AGENT + - pattern: delete globalThis.GLOBAL_AGENT + - pattern: delete global["GLOBAL_AGENT"] + - pattern: delete globalThis["GLOBAL_AGENT"] + - pattern: delete global.GLOBAL_AGENT.$KEY + - pattern: delete global.GLOBAL_AGENT[$KEY] + - pattern: delete globalThis.GLOBAL_AGENT.$KEY + - pattern: delete globalThis.GLOBAL_AGENT[$KEY] + - pattern: Object.defineProperty(global.GLOBAL_AGENT, $KEY, ...) + - pattern: Object.defineProperty(globalThis.GLOBAL_AGENT, $KEY, ...) + - pattern: | + Object.assign(global.GLOBAL_AGENT, { $KEY: ... }) + - pattern: | + Object.assign(globalThis.GLOBAL_AGENT, { $KEY: ... }) + - metavariable-regex: + metavariable: $KEY + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|NO_PROXY)["']?$ + - pattern-not-inside: | + function restoreGlobalAgentRuntime(...) { + ... + } + - pattern-not-inside: | + function restoreNodeHttpStack(...) { + ... + } + - pattern-not-inside: | + function bootstrapNodeHttpStack(...) { + ... + } + - pattern-not-inside: | + function writeGlobalAgentNoProxy(...) { + ... + } + - pattern-not-inside: | + function disableGlobalAgentProxyForIpv6GatewayLoopback(...) { + ... + } + + - id: managed-proxy-global-agent-object-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may replace or delete GLOBAL_AGENT runtime state. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - "CWE-441" + category: security + confidence: HIGH + paths: + include: + - "src/**/*.ts" + - "src/**/*.mts" + - "src/**/*.js" + - "src/**/*.mjs" + - "extensions/**/*.ts" + - "extensions/**/*.mts" + - "extensions/**/*.js" + - "extensions/**/*.mjs" + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - pattern: global.GLOBAL_AGENT = ... + - pattern: globalThis.GLOBAL_AGENT = ... + - pattern: global["GLOBAL_AGENT"] = ... + - pattern: globalThis["GLOBAL_AGENT"] = ... + - pattern: delete global.GLOBAL_AGENT + - pattern: delete globalThis.GLOBAL_AGENT + - pattern: delete global["GLOBAL_AGENT"] + - pattern: delete globalThis["GLOBAL_AGENT"] + - patterns: + - pattern-inside: | + const $GLOBAL = global; + ... + - pattern-either: + - pattern: $GLOBAL.GLOBAL_AGENT = ... + - pattern: $GLOBAL["GLOBAL_AGENT"] = ... + - pattern: delete $GLOBAL.GLOBAL_AGENT + - pattern: delete $GLOBAL["GLOBAL_AGENT"] + - patterns: + - pattern-inside: | + const $GLOBAL = global as $TYPE; + ... + - pattern-either: + - pattern: $GLOBAL.GLOBAL_AGENT = ... + - pattern: $GLOBAL["GLOBAL_AGENT"] = ... + - pattern: delete $GLOBAL.GLOBAL_AGENT + - pattern: delete $GLOBAL["GLOBAL_AGENT"] + - pattern-not-inside: | + function restoreNodeHttpStack(...) { + ... + } + + - id: managed-proxy-global-agent-alias-mutation + languages: + - typescript + - javascript + severity: ERROR + message: Only managed proxy owner scopes may mutate GLOBAL_AGENT aliases. + metadata: + advisory-id: OPENCLAW-POLICY-MANAGED-PROXY-RUNTIME-MUTATION + advisory-url: https://github.com/openclaw/openclaw/pull/77126 + cwe: + - "CWE-441" + category: security + confidence: HIGH + paths: + include: + - "src/**/*.ts" + - "src/**/*.mts" + - "src/**/*.js" + - "src/**/*.mjs" + - "extensions/**/*.ts" + - "extensions/**/*.mts" + - "extensions/**/*.js" + - "extensions/**/*.mjs" + exclude: + - "**/*.test.*" + - "**/*.spec.*" + - "**/*.min.js" + - "**/vendor/**" + patterns: + - pattern-either: + - patterns: + - pattern-inside: | + const $AGENT = global.GLOBAL_AGENT; + ... + - pattern-either: + - pattern: $AGENT.$KEY = ... + - pattern: $AGENT[$KEY] = ... + - pattern: delete $AGENT.$KEY + - pattern: delete $AGENT[$KEY] + - pattern: Object.defineProperty($AGENT, $KEY, ...) + - pattern: | + Object.assign($AGENT, { $KEY: ... }) + - patterns: + - pattern-inside: | + const $AGENT = global["GLOBAL_AGENT"]; + ... + - pattern-either: + - pattern: $AGENT.$KEY = ... + - pattern: $AGENT[$KEY] = ... + - pattern: delete $AGENT.$KEY + - pattern: delete $AGENT[$KEY] + - pattern: Object.defineProperty($AGENT, $KEY, ...) + - pattern: | + Object.assign($AGENT, { $KEY: ... }) + - patterns: + - pattern-inside: | + const $AGENT = (global as $TYPE)["GLOBAL_AGENT"] as $AGENT_TYPE; + ... + - pattern-either: + - pattern: $AGENT.$KEY = ... + - pattern: $AGENT[$KEY] = ... + - pattern: delete $AGENT.$KEY + - pattern: delete $AGENT[$KEY] + - pattern: Object.defineProperty($AGENT, $KEY, ...) + - pattern: | + Object.assign($AGENT, { $KEY: ... }) + - patterns: + - pattern-inside: | + const $GLOBAL = global; + ... + - pattern-either: + - pattern: $GLOBAL.GLOBAL_AGENT = ... + - pattern: $GLOBAL["GLOBAL_AGENT"] = ... + - pattern: $GLOBAL.GLOBAL_AGENT.$KEY = ... + - pattern: $GLOBAL.GLOBAL_AGENT[$KEY] = ... + - pattern: $GLOBAL["GLOBAL_AGENT"][$KEY] = ... + - pattern: delete $GLOBAL.GLOBAL_AGENT + - pattern: delete $GLOBAL["GLOBAL_AGENT"] + - pattern: delete $GLOBAL.GLOBAL_AGENT.$KEY + - pattern: delete $GLOBAL.GLOBAL_AGENT[$KEY] + - pattern: delete $GLOBAL["GLOBAL_AGENT"][$KEY] + - patterns: + - pattern-inside: | + const $GLOBAL = global as $TYPE; + ... + - pattern-either: + - pattern: $GLOBAL.GLOBAL_AGENT = ... + - pattern: $GLOBAL["GLOBAL_AGENT"] = ... + - pattern: $GLOBAL.GLOBAL_AGENT.$KEY = ... + - pattern: $GLOBAL.GLOBAL_AGENT[$KEY] = ... + - pattern: $GLOBAL["GLOBAL_AGENT"][$KEY] = ... + - pattern: delete $GLOBAL.GLOBAL_AGENT + - pattern: delete $GLOBAL["GLOBAL_AGENT"] + - pattern: delete $GLOBAL.GLOBAL_AGENT.$KEY + - pattern: delete $GLOBAL.GLOBAL_AGENT[$KEY] + - pattern: delete $GLOBAL["GLOBAL_AGENT"][$KEY] + - metavariable-regex: + metavariable: $KEY + regex: ^["']?(HTTP_PROXY|HTTPS_PROXY|NO_PROXY)["']?$ + - pattern-not-inside: | + function restoreGlobalAgentRuntime(...) { + ... + } + - pattern-not-inside: | + function restoreNodeHttpStack(...) { + ... + } + - pattern-not-inside: | + function bootstrapNodeHttpStack(...) { + ... + } + - pattern-not-inside: | + function writeGlobalAgentNoProxy(...) { + ... + } + - pattern-not-inside: | + function disableGlobalAgentProxyForIpv6GatewayLoopback(...) { + ... + } diff --git a/test/scripts/check-managed-proxy-runtime-mutation.test.ts b/test/scripts/check-managed-proxy-runtime-mutation.test.ts deleted file mode 100644 index 7774e7c5eaa..00000000000 --- a/test/scripts/check-managed-proxy-runtime-mutation.test.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - findManagedProxyRuntimeMutationLines, - findManagedProxyRuntimeMutations, - isAllowedManagedProxyRuntimeMutation, -} from "../../scripts/check-managed-proxy-runtime-mutation.mjs"; - -describe("check-managed-proxy-runtime-mutation", () => { - it("finds assignments and deletes for proxy env vars", () => { - const source = ` - process.env.HTTP_PROXY = "http://proxy"; - process.env["HTTPS_PROXY"] = "http://proxy"; - delete process.env.NO_PROXY; - delete process.env["GLOBAL_AGENT_NO_PROXY"]; - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4, 5]); - }); - - it("finds global object alias GLOBAL_AGENT mutations", () => { - const source = ` - const globalRecord = global; - const agent = globalRecord.GLOBAL_AGENT; - globalRecord.GLOBAL_AGENT = {}; - globalRecord["GLOBAL_AGENT"] = {}; - delete globalRecord.GLOBAL_AGENT; - delete globalRecord["GLOBAL_AGENT"]; - agent.HTTP_PROXY = "http://proxy"; - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([4, 5, 6, 7, 8]); - }); - - it("finds GLOBAL_AGENT mutations", () => { - const source = ` - global.GLOBAL_AGENT = {}; - global.GLOBAL_AGENT.NO_PROXY = "localhost"; - global["GLOBAL_AGENT"].HTTP_PROXY = "http://proxy"; - delete global.GLOBAL_AGENT.HTTPS_PROXY; - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4, 5]); - }); - - it("finds Object.assign and Object.defineProperty mutations", () => { - const source = ` - Object.assign(global.GLOBAL_AGENT, { NO_PROXY: "localhost" }); - Object.assign(process.env, { NO_PROXY: "localhost" }); - Object.defineProperty(process.env, "HTTP_PROXY", { value: "http://proxy" }); - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4]); - }); - - it("finds missing managed-proxy env key mutations", () => { - const source = ` - process.env.GLOBAL_AGENT_FORCE_GLOBAL_AGENT = "true"; - process.env.OPENCLAW_PROXY_LOOPBACK_MODE = "gateway-only"; - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3]); - }); - - it("finds defineProperty mutations with constant proxy keys", () => { - const source = ` - const proxyKey = "HTTP_PROXY"; - const agentKey = "NO_PROXY"; - Object.defineProperty(process.env, proxyKey, { value: "http://proxy" }); - Object.defineProperty(global.GLOBAL_AGENT, agentKey, { value: "localhost" }); - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([4, 5]); - }); - - it("finds destructured process.env alias mutations", () => { - const source = ` - const { env } = process; - env.HTTP_PROXY = "http://proxy"; - env["NO_PROXY"] = "localhost"; - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([3, 4]); - }); - - it("finds process.env alias and constant key mutations", () => { - const source = ` - const env = process.env; - const proxyKey = "HTTP_PROXY"; - env.HTTPS_PROXY = "http://proxy"; - env[proxyKey] = "http://proxy"; - delete env.NO_PROXY; - Object.assign(env, { GLOBAL_AGENT_HTTP_PROXY: "http://proxy" }); - Object.defineProperty(env, "OPENCLAW_PROXY_ACTIVE", { value: "1" }); - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([4, 5, 6, 7, 8]); - }); - - it("finds dynamic process.env key mutations from forbidden key arrays", () => { - const source = ` - const proxyKeys = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"]; - for (const key of proxyKeys) { - process.env[key] = "http://proxy"; - } - for (const key of proxyKeys) { - delete process.env[key]; - } - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([4, 7]); - }); - - it("finds dynamic process.env key mutations from spread-built forbidden key arrays", () => { - const source = ` - const lower = ["http_proxy", "https_proxy"]; - const upper = ["HTTP_PROXY", "HTTPS_PROXY"]; - const all = [...lower, ...upper, "OPENCLAW_PROXY_LOOPBACK_MODE"]; - for (const key of all) { - delete process.env[key]; - } - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([6]); - }); - - it("ignores dynamic process.env key mutations from unrelated key arrays", () => { - const source = ` - const normalKeys = ["PATH", "HOME"]; - for (const key of normalKeys) { - process.env[key] = "value"; - } - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([]); - }); - - it("finds GLOBAL_AGENT alias mutations", () => { - const source = ` - const agent = global.GLOBAL_AGENT; - agent.HTTP_PROXY = "http://proxy"; - agent["NO_PROXY"] = "localhost"; - delete agent.HTTPS_PROXY; - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([3, 4, 5]); - }); - - it("finds globalThis.GLOBAL_AGENT mutations alongside global.GLOBAL_AGENT", () => { - const source = ` - globalThis.GLOBAL_AGENT = {}; - globalThis.GLOBAL_AGENT.NO_PROXY = "localhost"; - globalThis["GLOBAL_AGENT"].HTTP_PROXY = "http://proxy"; - delete globalThis.GLOBAL_AGENT.HTTPS_PROXY; - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4, 5]); - }); - - it('finds process["env"] mixed access mutations', () => { - const source = ` - process["env"].HTTP_PROXY = "http://proxy"; - process["env"]["HTTPS_PROXY"] = "http://proxy"; - delete process["env"].NO_PROXY; - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([2, 3, 4]); - }); - - it("does not flag Object.assign on a non-process .env namespace", () => { - const source = ` - Object.assign(config.env, { NO_PROXY: "localhost" }); - Object.defineProperty(config.env, "HTTP_PROXY", { value: "http://proxy" }); - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([]); - }); - - it("ignores reads, unrelated env vars, comments, and strings", () => { - const source = ` - const current = process.env.HTTP_PROXY; - process.env.PATH = "/usr/bin"; - const text = "process.env.NO_PROXY = '*'"; - // global.GLOBAL_AGENT.NO_PROXY = '*'; - `; - - expect(findManagedProxyRuntimeMutationLines(source)).toEqual([]); - }); - - it("reports the enclosing owner scope for each mutation", () => { - const source = ` - function applyProxyEnv() { - process.env.HTTP_PROXY = "http://proxy"; - } - - class NoProxyLeaseManager { - release() { - delete process.env.NO_PROXY; - } - } - - const updateProxy = () => { - global.GLOBAL_AGENT.NO_PROXY = "localhost"; - }; - `; - - expect(findManagedProxyRuntimeMutations(source)).toEqual([ - { line: 3, scope: "applyProxyEnv" }, - { line: 8, scope: "NoProxyLeaseManager.release" }, - { line: 13, scope: "updateProxy" }, - ]); - }); - - it("allows approved owner scopes without exact line allowlists", () => { - expect( - isAllowedManagedProxyRuntimeMutation({ - relativePath: "src/infra/net/proxy/proxy-lifecycle.ts", - scope: "applyProxyEnv", - }), - ).toBe(true); - expect( - isAllowedManagedProxyRuntimeMutation({ - relativePath: "extensions/browser/src/browser/cdp-proxy-bypass.ts", - scope: "NoProxyLeaseManager.release", - }), - ).toBe(true); - expect( - isAllowedManagedProxyRuntimeMutation({ - relativePath: "src/infra/net/proxy/proxy-lifecycle.ts", - scope: "startProxy", - }), - ).toBe(false); - }); -}); diff --git a/test/scripts/run-additional-boundary-checks.test.ts b/test/scripts/run-additional-boundary-checks.test.ts index a64cc411d64..3c866e58619 100644 --- a/test/scripts/run-additional-boundary-checks.test.ts +++ b/test/scripts/run-additional-boundary-checks.test.ts @@ -30,14 +30,6 @@ describe("run-additional-boundary-checks", () => { }); }); - it("runs managed proxy runtime mutation guard in CI", () => { - expect(BOUNDARY_CHECKS).toContainEqual({ - label: "lint:tmp:managed-proxy-runtime-mutation", - command: "pnpm", - args: ["run", "lint:tmp:managed-proxy-runtime-mutation"], - }); - }); - it("runs raw socket classification guard in CI", () => { expect(BOUNDARY_CHECKS).toContainEqual({ label: "lint:tmp:raw-socket-classification",