Skip to content

hardening: storage permissions, ATTACH path escaping, project-config privilege stripping#143

Open
tracycam wants to merge 3 commits into
cortexkit:masterfrom
tracycam:hardening/storage-and-config-security
Open

hardening: storage permissions, ATTACH path escaping, project-config privilege stripping#143
tracycam wants to merge 3 commits into
cortexkit:masterfrom
tracycam:hardening/storage-and-config-security

Conversation

@tracycam

@tracycam tracycam commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Three defense-in-depth hardenings, one commit each:

  1. Escape SQLite ATTACH path (tool-owner-backfill.ts): the OpenCode DB path was interpolated into ATTACH '...' unescaped; a path containing a single quote breaks the statement. Now escaped ('''; ATTACH does not support bound parameters). Test covers a path containing a quote.

  2. Storage permissions (storage-db.ts, embedding-local.ts): context.db holds raw conversation history. The storage dir (and models cache dir) are now created/chmod'd 0700 and context.db/-wal/-shm chmod'd 0600. Best-effort with try/catch, skipped on win32. Test asserts modes (skipped on win32).

  3. Project-config privilege stripping (project-security.ts): project-level config merges over user config, so a repo-supplied magic-context.jsonc could redirect embedding to an arbitrary endpoint (memory text is POSTed to ${endpoint}/embeddings), enable dreamer/sidekick (agents allowed bash/write/edit), or override historian routing. stripUnsafeProjectConfigFields() now also strips embedding, historian, dreamer, sidekick (whole objects) and memory.git_commit_indexing from project config — these are user-config-only now, consistent with the existing strip of auto_update/sqlite/hidden-agent fields. CONFIGURATION.md documents the trust boundary.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Hardens storage and config security and fixes a SQLite ATTACH path bug. Prevents local data leaks, blocks repo-driven privilege escalation, and makes backfill robust for paths with quotes.

  • Bug Fixes
    • Escape SQLite ATTACH path in tool-owner backfill (handles single quotes; ATTACH can’t use bound params).
    • Restrict storage permissions: storage dir 0700; context.db/-wal/-shm 0600 (best-effort; skipped on Windows).
    • Strip unsafe project-config keys: drop embedding, historian, dreamer, sidekick (entire blocks), and memory.git_commit_indexing; disable {env:}/{file:} token expansion in project config; document the trust boundary in CONFIGURATION.md.

Written for commit 55e0bb6. Summary will update on new commits.

Review in cubic

Greptile Summary

Three independent hardening commits: SQLite ATTACH path escaping, filesystem permission tightening on the storage dir and DB files, and a stronger project-config privilege strip that now drops entire embedding/historian/dreamer/sidekick blocks (previously only dangerous sub-fields were removed). All changes are well-tested and the documentation is updated.

  • ATTACH escaping (tool-owner-backfill.ts): replaceAll("'", "''") is the correct SQL-literal escape for a context where bound parameters are unavailable; regression test covers an end-to-end path with a quote in the directory name.
  • Storage permissions (storage-db.ts, embedding-local.ts): storage dir created/chmoded to 0700, DB file and WAL/SHM sidecars to 0600; best-effort, Windows-skipped, chmod placed after WAL-mode init so sidecars exist before the call.
  • Project-config stripping (project-security.ts): upgrading from field-level to block-level strip closes routing-override and enablement vectors the previous approach left open; however, dropInheritedEmbeddingKeyOnRedirect is now unreachable in production (see inline comment).

Confidence Score: 4/5

Safe to merge — all three hardening changes behave correctly and the tests cover the key scenarios. One function in project-security.ts is now unreachable in practice and should be cleaned up in a follow-up.

The ATTACH escaping and permission tightening are straightforward and well-tested. The project-config strip is the most significant change and is correct in its effect, but it leaves dropInheritedEmbeddingKeyOnRedirect as dead code: both call sites in config/index.ts pass the already-stripped projectRaw, so the function's early-return guard fires on every invocation and the key-drop logic never runs. This does not reopen any protection gap (the full-block strip is strictly stronger), but it creates a misleading safety net that future contributors may rely on or be confused by.

packages/plugin/src/config/project-security.ts — dropInheritedEmbeddingKeyOnRedirect and its two call sites in config/index.ts can be removed.

Important Files Changed

Filename Overview
packages/plugin/src/features/magic-context/tool-owner-backfill.ts Adds single-quote escaping via replaceAll("'", "''") before ATTACH — correct standard SQL literal escape for a context that does not support bound parameters.
packages/plugin/src/config/project-security.ts Hardens project config stripping to drop entire embedding/historian/dreamer/sidekick blocks and memory.git_commit_indexing; correct and thorough, but dropInheritedEmbeddingKeyOnRedirect is now dead code (see comment).
packages/plugin/src/features/magic-context/storage-db.ts Adds ensureSecureStorageDir (mkdir + chmod 0700) and restrictDatabaseFilePermissions (chmod 0600 on db/-wal/-shm); best-effort, Windows-skipped, correctly placed after WAL-mode init creates sidecars.
packages/plugin/src/features/magic-context/memory/embedding-local.ts Adds mkdir mode 0o700 + best-effort chmodSync on the model cache dir, consistent with the storage-db pattern.
packages/plugin/src/features/magic-context/tool-owner-backfill.test.ts New test exercises ATTACH with a quote-containing path end-to-end; XDG_DATA_HOME is correctly restored by the existing afterEach.
packages/plugin/src/features/magic-context/storage-db.test.ts New test asserts 0o700 on storage dir and 0o600 on DB files; correctly skipped on win32.
packages/plugin/src/config/project-security.test.ts Updated tests correctly cover the new full-block strip behavior; dropInheritedEmbeddingKeyOnRedirect tests still pass but test the function in isolation rather than through the production call path where it is now unreachable.
packages/plugin/src/config/index.test.ts Token-expansion tests updated to use non-stripped fields (cache_ttl/disabled_hooks); new user-only-settings tests cover dreamer and memory.git_commit_indexing stripping.
CONFIGURATION.md Adds trust boundary documentation for project config; accurately describes the new strip set and token-expansion behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[loadPluginConfig called] --> B[Load user config\nexpand tokens]
    A --> C[Load project config\nNO token expansion]

    C --> D[stripUnsafeProjectConfigFields\nprojectRaw mutated in-place]
    D -->|deletes| E["auto_update\nsqlite\nembedding ← NEW\nhistorian ← NEW\ndreamer ← NEW\nsidekick ← NEW\nmemory.git_commit_indexing ← NEW"]

    D --> F[deepMergeRawConfig\nuser + project]
    B --> F

    F --> G[dropInheritedEmbeddingKeyOnRedirect\n⚠ now dead code:\nprojectRaw.embedding always absent]

    G --> H[parsePluginConfig / Zod]
    H --> I[MagicContextConfig returned]

    style E fill:#f9d,stroke:#c66
    style G fill:#ffd,stroke:#aa0
Loading

Comments Outside Diff (1)

  1. packages/plugin/src/config/project-security.ts, line 119-161 (link)

    P2 dropInheritedEmbeddingKeyOnRedirect is now dead code in every production call path. Both call sites in config/index.ts call stripUnsafeProjectConfigFields(projectRaw) first, which deletes projectRaw.embedding unconditionally. The very first guard in this function (if (!isPlainObject(projectRaw.embedding)) return []) will always short-circuit because embedding is absent by the time the function is called. The function provides zero additional protection and its unit tests in project-security.test.ts test it in isolation (without stripping first), so they do not surface the dead-code condition. Consider removing the function and its call sites now that the full-strip approach supersedes it.

Reviews (1): Last reviewed commit: "plugin: strip agent and embedding overri..." | Re-trigger Greptile

Copilot AI review requested due to automatic review settings June 12, 2026 19:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR hardens Magic Context against local data leaks and untrusted project config, and fixes a SQLite ATTACH path escaping edge case.

Changes:

  • Escape single quotes in the OpenCode DB path used in SQLite ATTACH, with a regression test.
  • Enforce tighter filesystem permissions for the Magic Context storage dir, DB, and SQLite sidecars, with tests.
  • Strengthen the project-config trust boundary by stripping additional user-only fields (embedding + hidden-agent blocks + git history indexing) and updating docs/tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/plugin/src/features/magic-context/tool-owner-backfill.ts Escapes ' in interpolated SQLite ATTACH path.
packages/plugin/src/features/magic-context/tool-owner-backfill.test.ts Adds regression test for quoted-path ATTACH.
packages/plugin/src/features/magic-context/storage-db.ts Creates/locks down storage dir and DB file permissions (POSIX).
packages/plugin/src/features/magic-context/storage-db.test.ts Adds POSIX permission enforcement test.
packages/plugin/src/features/magic-context/memory/embedding-local.ts Creates model cache dir with owner-only perms (best-effort).
packages/plugin/src/config/project-security.ts Expands stripping of unsafe project-config fields (embedding/agents/git indexing).
packages/plugin/src/config/project-security.test.ts Updates tests to match stronger stripping behavior.
packages/plugin/src/config/index.test.ts Updates config-merging/token tests to reflect new stripping rules.
CONFIGURATION.md Documents project-config trust boundary and which fields are user-only.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

closeQuietly(mc);
});

test("ATTACH succeeds when the OpenCode DB path contains a single quote", () => {
Comment on lines +533 to +537
const fs = require("node:fs");
const base = createTempDir("mc-backfill-quote-");
const dataHome = join(base, "o'brien");
fs.mkdirSync(dataHome, { recursive: true });
process.env.XDG_DATA_HOME = dataHome;
Comment on lines +406 to +412
if (process.platform !== "win32") {
try {
chmodSync(modelCacheDir, 0o700);
} catch {
// Non-fatal — leave default perms if chmod is rejected.
}
}
Comment on lines +64 to 80
if ("embedding" in projectRaw) {
delete projectRaw.embedding;
warnings.push(
"Ignoring embedding from project config (security: the embedding endpoint/provider/key is " +
"user-config-only; a repository must not redirect where memory text is sent).",
);
}

for (const agentKey of HIDDEN_AGENT_KEYS) {
const block = projectRaw[agentKey];
if (!isPlainObject(block)) continue;
const removed: string[] = [];
for (const field of AGENT_ESCALATION_FIELDS) {
if (field in block) {
delete block[field];
removed.push(field);
}
}
if (removed.length > 0) {
if (agentKey in projectRaw) {
delete projectRaw[agentKey];
warnings.push(
`Ignoring ${agentKey}.${removed.join("/")} from project config ` +
"(security: a repository cannot reprogram or re-permission hidden agents).",
`Ignoring ${agentKey} from project config (security: hidden agents are user-config-only; a ` +
"repository must not enable, reprogram, re-permission, or re-route the historian/dreamer/sidekick).",
);
}
}

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/plugin/src/features/magic-context/memory/embedding-local.ts">

<violation number="1" location="packages/plugin/src/features/magic-context/memory/embedding-local.ts:409">
P2: The chmod failure here is silently swallowed, unlike the equivalent hardening in `storage-db.ts` which logs the path and error message. Since this is a security hardening path (preventing group/world-readable model cache), a silent failure leaves sensitive data exposed with no operator visibility. Log the error to match the pattern used in `ensureSecureStorageDir`/`restrictDatabaseFilePermissions`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if (process.platform !== "win32") {
try {
chmodSync(modelCacheDir, 0o700);
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The chmod failure here is silently swallowed, unlike the equivalent hardening in storage-db.ts which logs the path and error message. Since this is a security hardening path (preventing group/world-readable model cache), a silent failure leaves sensitive data exposed with no operator visibility. Log the error to match the pattern used in ensureSecureStorageDir/restrictDatabaseFilePermissions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/src/features/magic-context/memory/embedding-local.ts, line 409:

<comment>The chmod failure here is silently swallowed, unlike the equivalent hardening in `storage-db.ts` which logs the path and error message. Since this is a security hardening path (preventing group/world-readable model cache), a silent failure leaves sensitive data exposed with no operator visibility. Log the error to match the pattern used in `ensureSecureStorageDir`/`restrictDatabaseFilePermissions`.</comment>

<file context>
@@ -399,7 +399,17 @@ export class LocalEmbeddingProvider implements EmbeddingProvider {
+                    if (process.platform !== "win32") {
+                        try {
+                            chmodSync(modelCacheDir, 0o700);
+                        } catch {
+                            // Non-fatal — leave default perms if chmod is rejected.
+                        }
</file context>

@alfonso-magic-context

Copy link
Copy Markdown
Collaborator

Thanks — this is solid work. Parts 1 and 2 (escape the ATTACH path; tighten context.db to 0600 and the storage/cache dirs to 0700) are clean defense-in-depth and we'd like to take them.

Could you drop part 3 (stripping the whole embedding/historian/dreamer/sidekick objects + memory.git_commit_indexing from project config) from this PR for now? It changes a deliberate trust-boundary decision rather than fixing a bug, and we want to handle it as separate, focused work:

  • Today's policy (project-security.ts) intentionally lets a repo tune its own dreamer/historian model/cadence/tasks — those run through the user's own provider auth and aren't an escalation vector — while stripping the actual escalation fields (prompt/permission/tools/system_prompt).
  • The embedding-key exfiltration concern is already handled precisely by dropInheritedEmbeddingKeyOnRedirect, which drops the inherited api_key only when a project redirects embedding.endpoint — without breaking a project that legitimately points at its own endpoint with its own key.

Stripping those blocks wholesale would remove legitimate per-repo configurability, so we'd rather design the boundary deliberately (likely a more granular allow-list) in its own change. If you rebase this down to parts 1 + 2 we can merge it quickly. Appreciate the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants