Skip to content

Commit 8b918cb

Browse files
anandgupta42claude
andcommitted
fix: harden Altimate Memory against path traversal, add adversarial tests
Security fixes: - Replace permissive ID regex with segment-based validation that rejects '..', '.', '//', and all path traversal patterns (a/../b, a/./b, etc.) - Use unique temp file names (timestamp + random suffix) to prevent race condition crashes during concurrent writes to the same block ID The old regex /^[a-z0-9][a-z0-9_/.-]*[a-z0-9]$/ allowed dangerous IDs like "a/../b" or "a/./b" that could escape the memory directory via path.join(). The new regex validates each path segment individually. Adds 71 adversarial tests covering: - Path traversal attacks (10 tests) - Frontmatter injection and parsing edge cases (9 tests) - Unicode and special character handling (6 tests) - TTL/expiration boundary conditions (6 tests) - Deduplication edge cases (7 tests) - Concurrent operations and race conditions (4 tests) - ID validation gaps (11 tests) - Malformed files on disk (7 tests) - Serialization round-trip edge cases (5 tests) - Schema validation with adversarial inputs (6 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e1d5a5c commit 8b918cb

8 files changed

Lines changed: 894 additions & 21 deletions

File tree

packages/opencode/src/memory/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export namespace MemoryStore {
198198
const dir = path.dirname(filepath)
199199
await fs.mkdir(dir, { recursive: true })
200200

201-
const tmpPath = filepath + ".tmp"
201+
const tmpPath = filepath + `.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`
202202
const serialized = serializeBlock(block)
203203

204204
await fs.writeFile(tmpPath, serialized, "utf-8")

packages/opencode/src/memory/tools/memory-extract.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import z from "zod"
22
import { Tool } from "../../tool/tool"
33
import { MemoryStore } from "../store"
4+
import { MemoryBlockSchema } from "../types"
5+
6+
const idSchema = MemoryBlockSchema.shape.id
47

58
export const MemoryExtractTool = Tool.define("altimate_memory_extract", {
69
description:
@@ -9,11 +12,7 @@ export const MemoryExtractTool = Tool.define("altimate_memory_extract", {
912
facts: z
1013
.array(
1114
z.object({
12-
id: z
13-
.string()
14-
.min(1)
15-
.max(256)
16-
.regex(/^[a-z0-9][a-z0-9_/.-]*[a-z0-9]$|^[a-z0-9]$/),
15+
id: idSchema,
1716
scope: z.enum(["global", "project"]),
1817
content: z.string().min(1).max(2048),
1918
tags: z.array(z.string().max(64)).max(10).optional().default([]),

packages/opencode/src/memory/tools/memory-write.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import z from "zod"
22
import { Tool } from "../../tool/tool"
33
import { MemoryStore } from "../store"
4-
import { MEMORY_MAX_BLOCK_SIZE, MEMORY_MAX_BLOCKS_PER_SCOPE, CitationSchema } from "../types"
4+
import { MEMORY_MAX_BLOCK_SIZE, MEMORY_MAX_BLOCKS_PER_SCOPE, CitationSchema, MemoryBlockSchema } from "../types"
5+
6+
const idSchema = MemoryBlockSchema.shape.id
57

68
export const MemoryWriteTool = Tool.define("altimate_memory_write", {
79
description: `Save an Altimate Memory block for cross-session persistence. Use this to store information worth remembering across sessions — warehouse configurations, naming conventions, team preferences, data model notes, or past analysis decisions. Each block is a Markdown file persisted to disk. Max ${MEMORY_MAX_BLOCK_SIZE} chars per block, ${MEMORY_MAX_BLOCKS_PER_SCOPE} blocks per scope. Supports hierarchical IDs with slashes (e.g., 'warehouse/snowflake-config'), optional TTL expiration, and citation-backed memories.`,
810
parameters: z.object({
9-
id: z
10-
.string()
11-
.min(1)
12-
.max(256)
13-
.regex(/^[a-z0-9][a-z0-9_/.-]*[a-z0-9]$|^[a-z0-9]$/)
11+
id: idSchema
1412
.describe(
1513
"Unique identifier for this memory block (lowercase, hyphens/underscores/slashes for namespaces). Examples: 'warehouse-config', 'warehouse/snowflake', 'conventions/dbt-naming'",
1614
),

packages/opencode/src/memory/types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@ export const CitationSchema = z.object({
88

99
export type Citation = z.infer<typeof CitationSchema>
1010

11+
// Each path segment must start and end with alphanumeric.
12+
// Segments are separated by '/'. No '..' or '.' as standalone segments (prevents path traversal).
13+
// No double slashes, no leading/trailing slashes.
14+
const MEMORY_ID_SEGMENT = /[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?/
15+
const MEMORY_ID_REGEX = new RegExp(
16+
`^${MEMORY_ID_SEGMENT.source}(?:\\.${MEMORY_ID_SEGMENT.source})*(?:/${MEMORY_ID_SEGMENT.source}(?:\\.${MEMORY_ID_SEGMENT.source})*)*$`,
17+
)
18+
1119
export const MemoryBlockSchema = z.object({
12-
id: z.string().min(1).max(256).regex(/^[a-z0-9][a-z0-9_/.-]*[a-z0-9]$|^[a-z0-9]$/, {
13-
message: "ID must be lowercase alphanumeric with hyphens/underscores/slashes/dots, starting and ending with alphanumeric",
20+
id: z.string().min(1).max(256).regex(MEMORY_ID_REGEX, {
21+
message: "ID must be lowercase alphanumeric segments separated by '/' or '.', each starting/ending with alphanumeric. No '..' or empty segments allowed.",
1422
}),
1523
scope: z.enum(["global", "project"]),
1624
tags: z.array(z.string().max(64)).max(10).default([]),

0 commit comments

Comments
 (0)