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:
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Generated
+5
-5
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user