fix(openclaw): clear security scanner exfiltration warning (#4678)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
This commit is contained in:
Chaithanya Kumar
2026-04-03 00:34:49 +05:30
committed by GitHub
parent c0cae68646
commit 33d2bc495d
5 changed files with 115 additions and 11 deletions
+18
View File
@@ -2,6 +2,24 @@
All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file.
## [1.0.3] - 2026-04-03
### Fixed
- **Path traversal vulnerability**: Added `safePath()` containment helper to `readSkillFile` and `readDomainOverlay` in `skill-loader.ts` — prevents directory traversal via `config.domain` or the exported `loadSkill` API
- **Noise filter regression**: Reverted incorrect `After-Compaction` regex rename back to `Post-Compaction` so the filter correctly matches real upstream compaction audit messages
- **Cosmetic revert**: Restored `// Over-fetch for ranking` comment in `recall.ts` (was changed to work around a false-positive scanner match on the substring `fetch`)
### Changed
- **Supply-chain hardening**: Pinned `mem0ai` dependency to exact `2.3.0` (was `^2.3.0`)
### Added
- **Path traversal tests**: 12 new tests covering `safePath`, `readSkillFile`, `readDomainOverlay`, and `loadSkill` with traversal inputs
## [1.0.2] - 2026-04-02
### Fixed
- **Security scanner warning**: Removed `resolveEnvVars()` and `resolveEnvVarsDeep()` from `config.ts` — OpenClaw already resolves `${VAR}` in `openclaw.json` before passing config to the plugin, so plugin-side env resolution was redundant and triggered the "credential harvesting" static analysis warning ([#4676](https://github.com/mem0ai/mem0/pull/4676))
## [1.0.1] - 2026-04-02
### Added
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "1.0.2",
"version": "1.0.3",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
@@ -35,7 +35,7 @@
},
"dependencies": {
"@sinclair/typebox": "0.34.47",
"mem0ai": "^2.3.0"
"mem0ai": "2.3.0"
},
"openclaw": {
"extensions": [
+5 -5
View File
@@ -12,8 +12,8 @@ importers:
specifier: 0.34.47
version: 0.34.47
mem0ai:
specifier: ^2.3.0
version: 2.4.0(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(groq-sdk@0.3.0)(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.19.0)
specifier: 2.3.0
version: 2.3.0(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(groq-sdk@0.3.0)(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.19.0)
devDependencies:
'@types/node':
specifier: ^22.15.0
@@ -1466,8 +1466,8 @@ packages:
md5@2.3.0:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
mem0ai@2.4.0:
resolution: {integrity: sha512-b1V8KXUse8ySvio8D0bRHOvgxa9BlmP4g4bjtEnA3uILcu+2PAtcAiHDPdBxUIeoxTxAeJM+0wBGaMIps4bIaw==}
mem0ai@2.3.0:
resolution: {integrity: sha512-9e0B9hgM7nxnKDD6zmSRkfkrQ2sa9tGdpZE3CPsp7eIu18soYbXvm9YT08mllCYuiW+EdEW7L/+DNCphOfvkKQ==}
engines: {node: '>=18'}
peerDependencies:
'@anthropic-ai/sdk': ^0.40.1
@@ -3543,7 +3543,7 @@ snapshots:
crypt: 0.0.2
is-buffer: 1.1.6
mem0ai@2.4.0(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(groq-sdk@0.3.0)(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.19.0):
mem0ai@2.3.0(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(groq-sdk@0.3.0)(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.19.0):
dependencies:
'@anthropic-ai/sdk': 0.40.1
'@azure/identity': 4.13.0
+70
View File
@@ -0,0 +1,70 @@
/**
* Tests for path traversal prevention in skill-loader.
*/
import { describe, it, expect } from "vitest";
import { safePath, loadSkill } from "./skill-loader.ts";
// ---------------------------------------------------------------------------
// safePath — path containment
// ---------------------------------------------------------------------------
describe("safePath", () => {
it("rejects parent directory traversal", () => {
expect(safePath("../../etc/passwd")).toBeNull();
});
it("rejects deep traversal", () => {
expect(safePath("../../../etc/shadow")).toBeNull();
});
it("rejects traversal in nested segment", () => {
expect(safePath("valid", "../../etc")).toBeNull();
});
it("rejects bare '..' as segment", () => {
expect(safePath("..")).toBeNull();
});
it("accepts valid skill paths", () => {
expect(safePath("memory-triage", "SKILL.md")).not.toBeNull();
});
it("accepts valid domain overlay paths", () => {
expect(safePath("memory-triage", "domains", "companion.md")).not.toBeNull();
});
it("returns null for empty segments that resolve to skills root with subpath escape", () => {
// path.resolve("skills", "", "../../etc") still escapes
expect(safePath("", "../../etc")).toBeNull();
});
it("rejects traversal disguised with valid prefix", () => {
expect(safePath("memory-triage/../../etc/passwd")).toBeNull();
});
});
// ---------------------------------------------------------------------------
// loadSkill — integration tests for traversal prevention
// ---------------------------------------------------------------------------
describe("loadSkill path traversal", () => {
it("returns null for traversal skillName", () => {
expect(loadSkill("../../etc/passwd")).toBeNull();
});
it("returns null for deep traversal skillName", () => {
expect(loadSkill("../../../..")).toBeNull();
});
it("loads a valid skill", () => {
const result = loadSkill("memory-triage");
expect(result).not.toBeNull();
expect(result?.prompt).toBeTruthy();
});
it("blocks domain traversal while loading valid skill", () => {
// Valid skill name, malicious domain — should load skill but skip the overlay
const result = loadSkill("memory-triage", { domain: "../../etc/passwd" });
// Should still succeed (skill itself is valid), domain overlay is just skipped
expect(result).not.toBeNull();
expect(result?.prompt).toBeTruthy();
});
});
+20 -4
View File
@@ -110,10 +110,26 @@ function resolveSkillsDir(): string {
}
const SKILLS_DIR = resolveSkillsDir();
const RESOLVED_SKILLS_DIR = path.resolve(SKILLS_DIR);
/**
* Resolve path segments under SKILLS_DIR and verify the result doesn't escape.
* Returns null if the resolved path is outside the skills directory (path traversal).
* Note: path.resolve follows symlinks lexically; the skills directory is
* package-owned so symlink escape is not a practical concern.
*/
export function safePath(...segments: string[]): string | null {
const resolved = path.resolve(SKILLS_DIR, ...segments);
if (resolved !== RESOLVED_SKILLS_DIR &&
!resolved.startsWith(RESOLVED_SKILLS_DIR + path.sep)) {
return null;
}
return resolved;
}
function readSkillFile(skillName: string): string | null {
// Skills use OpenClaw directory format: <skill-name>/SKILL.md
const filePath = path.join(SKILLS_DIR, skillName, "SKILL.md");
const filePath = safePath(skillName, "SKILL.md");
if (!filePath) return null;
try {
return fs.readFileSync(filePath, "utf-8");
} catch {
@@ -127,8 +143,8 @@ function readSkillFile(skillName: string): string | null {
* The `applies_to` frontmatter field is checked for backward compatibility.
*/
function readDomainOverlay(domain: string, targetSkill: string): string | null {
// Domain overlays are stored inside the target skill's directory
const filePath = path.join(SKILLS_DIR, targetSkill, "domains", `${domain}.md`);
const filePath = safePath(targetSkill, "domains", `${domain}.md`);
if (!filePath) return null;
try {
const content = fs.readFileSync(filePath, "utf-8");
const parsed = parseSkillFile(content);