Files
openclaw/src/plugins/dependency-denylist.ts
Michael Appel 9f97ad857a fix(security): pin axios to 1.15.0 and add dependency denylist for plugin installs [AI-assisted] (#63891)
* fix: address issue

* fix: address review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* Plugins: fix install security CI regressions

* Plugins: make manifest traversal linear

* Plugins: bound manifest security traversal

* Plugins: block denied node_modules package dirs

* Plugins: match node_modules case-insensitively

* Plugins: block denied package symlink paths

* Tests: normalize blocked symlink assertion

* Plugins: fail closed on unreadable denied paths

* Plugins: block denied node_modules file aliases

* Plugins: inspect node_modules symlink targets

* Plugins: preserve symlink target package paths

* fix: address PR review feedback

* chore(changelog): add axios pin and dependency denylist entry

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-10 11:20:05 -06:00

327 lines
9.7 KiB
TypeScript

const BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAMES = ["plain-crypto-js"] as const;
export const blockedInstallDependencyPackageNames = [
...BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAMES,
] as const;
export type BlockedManifestDependencyFinding = {
dependencyName: string;
declaredAs?: string;
field: "dependencies" | "name" | "optionalDependencies" | "overrides" | "peerDependencies";
};
export type BlockedPackageDirectoryFinding = {
dependencyName: string;
directoryRelativePath: string;
};
export type BlockedPackageFileFinding = {
dependencyName: string;
fileRelativePath: string;
};
type PackageDependencyMapFields = Partial<
Record<
Exclude<BlockedManifestDependencyFinding["field"], "name" | "overrides">,
Record<string, string>
>
>;
type PackageDependencyFields = {
name?: string;
} & PackageDependencyMapFields;
interface PackageOverrideObject {
[key: string]: PackageOverrideValue;
}
type PackageOverrideValue = string | PackageOverrideObject;
type PackageOverrideFields = {
overrides?: unknown;
};
const BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAME_SET = new Set<string>(
blockedInstallDependencyPackageNames,
);
const BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAME_LOWER_SET = new Set<string>(
blockedInstallDependencyPackageNames.map((packageName) => packageName.toLowerCase()),
);
function isBlockedInstallDependencyPackageName(packageName: string): boolean {
return BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAME_SET.has(packageName);
}
function isBlockedInstallDependencyPackagePathName(packageName: string): boolean {
return BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAME_LOWER_SET.has(packageName.toLowerCase());
}
function normalizePathSegments(relativePath: string): string[] {
return relativePath
.split(/[\\/]+/)
.map((segment) => segment.trim())
.filter(Boolean);
}
function parseBlockedNodeModulesPackageId(
segments: string[],
packageNameSegmentTransform: (packageNameSegment: string) => string | undefined,
): string | undefined {
for (let index = 0; index < segments.length; index += 1) {
if (segments[index]?.toLowerCase() !== "node_modules") {
continue;
}
const packageScopeOrName = segments[index + 1];
if (!packageScopeOrName) {
continue;
}
if (packageScopeOrName.startsWith("@")) {
const packageNameSegment = segments[index + 2];
if (!packageNameSegment) {
continue;
}
const packageName = packageNameSegmentTransform(packageNameSegment);
if (!packageName) {
continue;
}
const scopedPackageId = `${packageScopeOrName}/${packageName}`;
if (!isBlockedInstallDependencyPackagePathName(scopedPackageId)) {
continue;
}
return scopedPackageId;
}
const packageName = packageNameSegmentTransform(packageScopeOrName);
if (!packageName || !isBlockedInstallDependencyPackagePathName(packageName)) {
continue;
}
return packageName;
}
return undefined;
}
function parseNpmAliasTargetPackageName(spec: string): string | undefined {
const normalized = spec.trim();
if (!normalized.startsWith("npm:")) {
return undefined;
}
const aliasTarget = normalized.slice("npm:".length).trim();
if (!aliasTarget) {
return undefined;
}
if (aliasTarget.startsWith("@")) {
const slashIndex = aliasTarget.indexOf("/");
if (slashIndex < 0) {
return undefined;
}
const versionSeparatorIndex = aliasTarget.indexOf("@", slashIndex + 1);
return versionSeparatorIndex < 0 ? aliasTarget : aliasTarget.slice(0, versionSeparatorIndex);
}
const versionSeparatorIndex = aliasTarget.indexOf("@");
return versionSeparatorIndex < 0 ? aliasTarget : aliasTarget.slice(0, versionSeparatorIndex);
}
function parsePackageNameFromOverrideSelector(selector: string): string | undefined {
const normalized = selector.trim();
if (!normalized || normalized === ".") {
return undefined;
}
if (normalized.startsWith("@")) {
const slashIndex = normalized.indexOf("/");
if (slashIndex < 0) {
return undefined;
}
const versionSeparatorIndex = normalized.indexOf("@", slashIndex + 1);
return versionSeparatorIndex < 0 ? normalized : normalized.slice(0, versionSeparatorIndex);
}
const versionSeparatorIndex = normalized.indexOf("@");
return versionSeparatorIndex < 0 ? normalized : normalized.slice(0, versionSeparatorIndex);
}
function collectBlockedOverrideFindings(
value: PackageOverrideValue,
path: string[] = [],
): BlockedManifestDependencyFinding[] {
if (typeof value === "string") {
const aliasTargetPackageName = parseNpmAliasTargetPackageName(value);
if (!aliasTargetPackageName) {
return [];
}
if (!BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAME_SET.has(aliasTargetPackageName)) {
return [];
}
return [
{
dependencyName: aliasTargetPackageName,
declaredAs: path.join(" > "),
field: "overrides",
},
];
}
const findings: BlockedManifestDependencyFinding[] = [];
for (const overrideKey of Object.keys(value).toSorted()) {
const overrideSelectorPackageName = parsePackageNameFromOverrideSelector(overrideKey);
if (
overrideSelectorPackageName &&
BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAME_SET.has(overrideSelectorPackageName)
) {
findings.push({
dependencyName: overrideSelectorPackageName,
declaredAs: [...path, overrideKey].join(" > "),
field: "overrides",
});
}
findings.push(...collectBlockedOverrideFindings(value[overrideKey], [...path, overrideKey]));
}
return findings;
}
function isPackageOverrideObject(value: unknown): value is PackageOverrideObject {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function findBlockedManifestDependencies(
manifest: PackageDependencyFields & PackageOverrideFields,
): BlockedManifestDependencyFinding[] {
const findings: BlockedManifestDependencyFinding[] = [];
if (manifest.name && isBlockedInstallDependencyPackageName(manifest.name)) {
findings.push({ dependencyName: manifest.name, field: "name" });
}
if (isPackageOverrideObject(manifest.overrides)) {
findings.push(...collectBlockedOverrideFindings(manifest.overrides));
}
for (const field of ["dependencies", "optionalDependencies", "peerDependencies"] as const) {
const dependencyMap = manifest[field];
if (!dependencyMap) {
continue;
}
for (const dependencyName of Object.keys(dependencyMap).toSorted()) {
if (isBlockedInstallDependencyPackageName(dependencyName)) {
findings.push({ dependencyName, field });
continue;
}
const aliasTargetPackageName = parseNpmAliasTargetPackageName(dependencyMap[dependencyName]);
if (!aliasTargetPackageName) {
continue;
}
if (!isBlockedInstallDependencyPackageName(aliasTargetPackageName)) {
continue;
}
findings.push({
dependencyName: aliasTargetPackageName,
declaredAs: dependencyName,
field,
});
}
}
return findings;
}
export function findBlockedNodeModulesDirectory(params: {
directoryRelativePath: string;
}): BlockedPackageDirectoryFinding | undefined {
const dependencyName = parseBlockedNodeModulesPackageId(
normalizePathSegments(params.directoryRelativePath),
(packageNameSegment) => packageNameSegment,
);
return dependencyName
? {
dependencyName,
directoryRelativePath: params.directoryRelativePath,
}
: undefined;
}
function parseBlockedPackageFileAliasName(fileName: string): string | undefined {
const extensionMatch = /^(.+)\.(js|json|node)$/i.exec(fileName);
if (extensionMatch) {
return extensionMatch[1];
}
if (fileName.includes(".")) {
return undefined;
}
return fileName;
}
export function findBlockedNodeModulesFileAlias(params: {
fileRelativePath: string;
}): BlockedPackageFileFinding | undefined {
const dependencyName = parseBlockedNodeModulesPackageId(
normalizePathSegments(params.fileRelativePath),
parseBlockedPackageFileAliasName,
);
return dependencyName
? {
dependencyName,
fileRelativePath: params.fileRelativePath,
}
: undefined;
}
export function findBlockedPackageDirectoryInPath(params: {
pathRelativeToRoot: string;
}): BlockedPackageDirectoryFinding | undefined {
const segments = normalizePathSegments(params.pathRelativeToRoot);
for (let index = 0; index < segments.length; index += 1) {
const packageScopeOrName = segments[index];
if (!packageScopeOrName) {
continue;
}
if (packageScopeOrName.startsWith("@")) {
const packageName = segments[index + 1];
if (!packageName) {
continue;
}
const scopedPackageId = `${packageScopeOrName}/${packageName}`;
if (!isBlockedInstallDependencyPackagePathName(scopedPackageId)) {
index += 1;
continue;
}
return {
dependencyName: scopedPackageId,
directoryRelativePath: params.pathRelativeToRoot,
};
}
if (!isBlockedInstallDependencyPackagePathName(packageScopeOrName)) {
continue;
}
return {
dependencyName: packageScopeOrName,
directoryRelativePath: params.pathRelativeToRoot,
};
}
return undefined;
}
export function findBlockedPackageFileAliasInPath(params: {
pathRelativeToRoot: string;
}): BlockedPackageFileFinding | undefined {
const segments = normalizePathSegments(params.pathRelativeToRoot);
const fileName = segments.at(-1);
if (!fileName) {
return undefined;
}
const dependencyName = parseBlockedPackageFileAliasName(fileName);
if (!dependencyName || !isBlockedInstallDependencyPackagePathName(dependencyName)) {
return undefined;
}
return {
dependencyName,
fileRelativePath: params.pathRelativeToRoot,
};
}