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:
co-authored by
Claude Opus 4.6
Saket Aryan
parent
c0cae68646
commit
33d2bc495d
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file.
|
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
|
## [1.0.1] - 2026-04-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mem0/openclaw-mem0",
|
"name": "@mem0/openclaw-mem0",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sinclair/typebox": "0.34.47",
|
"@sinclair/typebox": "0.34.47",
|
||||||
"mem0ai": "^2.3.0"
|
"mem0ai": "2.3.0"
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|||||||
Generated
+5
-5
@@ -12,8 +12,8 @@ importers:
|
|||||||
specifier: 0.34.47
|
specifier: 0.34.47
|
||||||
version: 0.34.47
|
version: 0.34.47
|
||||||
mem0ai:
|
mem0ai:
|
||||||
specifier: ^2.3.0
|
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)
|
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:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.15.0
|
specifier: ^22.15.0
|
||||||
@@ -1466,8 +1466,8 @@ packages:
|
|||||||
md5@2.3.0:
|
md5@2.3.0:
|
||||||
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
||||||
|
|
||||||
mem0ai@2.4.0:
|
mem0ai@2.3.0:
|
||||||
resolution: {integrity: sha512-b1V8KXUse8ySvio8D0bRHOvgxa9BlmP4g4bjtEnA3uILcu+2PAtcAiHDPdBxUIeoxTxAeJM+0wBGaMIps4bIaw==}
|
resolution: {integrity: sha512-9e0B9hgM7nxnKDD6zmSRkfkrQ2sa9tGdpZE3CPsp7eIu18soYbXvm9YT08mllCYuiW+EdEW7L/+DNCphOfvkKQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@anthropic-ai/sdk': ^0.40.1
|
'@anthropic-ai/sdk': ^0.40.1
|
||||||
@@ -3543,7 +3543,7 @@ snapshots:
|
|||||||
crypt: 0.0.2
|
crypt: 0.0.2
|
||||||
is-buffer: 1.1.6
|
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:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.40.1
|
'@anthropic-ai/sdk': 0.40.1
|
||||||
'@azure/identity': 4.13.0
|
'@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 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 {
|
function readSkillFile(skillName: string): string | null {
|
||||||
// Skills use OpenClaw directory format: <skill-name>/SKILL.md
|
const filePath = safePath(skillName, "SKILL.md");
|
||||||
const filePath = path.join(SKILLS_DIR, skillName, "SKILL.md");
|
if (!filePath) return null;
|
||||||
try {
|
try {
|
||||||
return fs.readFileSync(filePath, "utf-8");
|
return fs.readFileSync(filePath, "utf-8");
|
||||||
} catch {
|
} catch {
|
||||||
@@ -127,8 +143,8 @@ function readSkillFile(skillName: string): string | null {
|
|||||||
* The `applies_to` frontmatter field is checked for backward compatibility.
|
* The `applies_to` frontmatter field is checked for backward compatibility.
|
||||||
*/
|
*/
|
||||||
function readDomainOverlay(domain: string, targetSkill: string): string | null {
|
function readDomainOverlay(domain: string, targetSkill: string): string | null {
|
||||||
// Domain overlays are stored inside the target skill's directory
|
const filePath = safePath(targetSkill, "domains", `${domain}.md`);
|
||||||
const filePath = path.join(SKILLS_DIR, targetSkill, "domains", `${domain}.md`);
|
if (!filePath) return null;
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(filePath, "utf-8");
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
const parsed = parseSkillFile(content);
|
const parsed = parseSkillFile(content);
|
||||||
|
|||||||
Reference in New Issue
Block a user