From faa2bae1044e91c5b36ee1305e2319883d3eb3c1 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Mon, 6 Jul 2026 10:03:15 +0200 Subject: [PATCH] docs: clarify project {file:} rejects paths that escape the project root In-root absolute paths are intentionally allowed; only references that leave the root (absolute paths outside it, ../ traversal, symlinks) are rejected. Fix the docs wording and add a regression test for the in-root absolute case. --- .../code-with-ai/agents/custom-models.md | 2 +- .../pages/code-with-ai/platforms/cli.md | 2 +- .../test/kilocode/config/variable.test.ts | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md b/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md index 4dc20ddfa38..d489dc63ebc 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md @@ -391,7 +391,7 @@ You can also set options that apply to all models from a provider: | `chunkTimeout` | `number` | Timeout in milliseconds between streamed response chunks. If no chunk arrives within this window, the request is aborted and retried. This catches silent provider dropouts where the TCP connection stays open but SSE streaming stops. Recommended: `15000`–`30000` (15–30 seconds) for providers with unreliable streaming. | {% callout type="warning" title="{env:} / {file:} only resolve in trusted config" %} -`{env:VAR}` and `{file:...}` references in `apiKey` (or any option) are resolved **only** when the config lives in a trusted location: your global config (`~/.config/kilo`), a config passed via `KILO_CONFIG` / `KILO_CONFIG_CONTENT`, or organization/MDM-managed config. A project-level `kilo.json` / `opencode.json` committed to a repository **cannot** resolve `{env:VAR}` — the reference is ignored and a warning is logged, so a provider configured this way in a repo will not authenticate. This prevents a malicious repository from exfiltrating your secrets to an attacker-controlled `baseURL` just by being opened. `{file:...}` still works in project config, but only for files that stay inside the project root (absolute paths, `../` traversal, and symlink escapes are rejected). Keep provider credentials in your global config. +`{env:VAR}` and `{file:...}` references in `apiKey` (or any option) are resolved **only** when the config lives in a trusted location: your global config (`~/.config/kilo`), a config passed via `KILO_CONFIG` / `KILO_CONFIG_CONTENT`, or organization/MDM-managed config. A project-level `kilo.json` / `opencode.json` committed to a repository **cannot** resolve `{env:VAR}` — the reference is ignored and a warning is logged, so a provider configured this way in a repo will not authenticate. This prevents a malicious repository from exfiltrating your secrets to an attacker-controlled `baseURL` just by being opened. `{file:...}` still works in project config, but only for files that resolve inside the project root — references that leave it (absolute paths outside the root, `../` traversal, and symlink escapes) are rejected. Keep provider credentials in your global config. {% /callout %} ## Filtering Available Models diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md index dcf1cf0eb5b..750653c2f0e 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md @@ -465,7 +465,7 @@ Use `{env:VARIABLE_NAME}` syntax in config files to reference environment variab ``` {% callout type="warning" title="Only works in trusted config" %} -`{env:VAR}` (and `{file:...}`) references are resolved **only** in trusted config: your global config (`~/.config/kilo`), a config passed via `KILO_CONFIG` / `KILO_CONFIG_CONTENT`, or organization/MDM-managed config. A project-level `kilo.json` / `opencode.json` committed to a repository **cannot** use `{env:VAR}` — the reference is ignored and a warning is logged. This prevents a malicious repository from exfiltrating your secrets to an attacker-controlled `baseURL` simply by being opened. `{file:...}` still works in project config, but only for files that stay inside the project root. +`{env:VAR}` (and `{file:...}`) references are resolved **only** in trusted config: your global config (`~/.config/kilo`), a config passed via `KILO_CONFIG` / `KILO_CONFIG_CONTENT`, or organization/MDM-managed config. A project-level `kilo.json` / `opencode.json` committed to a repository **cannot** use `{env:VAR}` — the reference is ignored and a warning is logged. This prevents a malicious repository from exfiltrating your secrets to an attacker-controlled `baseURL` simply by being opened. `{file:...}` still works in project config, but only for files that resolve inside the project root — references that leave it (absolute paths outside the root, `../` traversal, and symlink escapes) are rejected. {% /callout %} For full details on all configuration options including compaction, file watchers, plugins, and experimental features, see the [OpenCode Config documentation](https://opencode.ai/docs/config). diff --git a/packages/opencode/test/kilocode/config/variable.test.ts b/packages/opencode/test/kilocode/config/variable.test.ts index 9e792dba764..5cbf347ce67 100644 --- a/packages/opencode/test/kilocode/config/variable.test.ts +++ b/packages/opencode/test/kilocode/config/variable.test.ts @@ -47,6 +47,25 @@ test("allows untrusted file references that stay inside the scope root", async ( } }) +test("allows untrusted absolute file references that resolve inside the scope root", async () => { + const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "kilo-config-variable-abs-inside-"))) + const file = path.join(root, "value") + await fs.writeFile(file, "allowed") + try { + // An absolute path is fine as long as it stays inside the root; only escapes are rejected. + expect( + await ConfigVariable.substitute({ + ...source, + dir: root, + text: `{file:${file}}`, + fileScope: { root, source: path.join(root, "kilo.json") }, + }), + ).toBe("allowed") + } finally { + await fs.rm(root, { recursive: true, force: true }) + } +}) + test("rejects environment references in untrusted (project) config", async () => { await expect( ConfigVariable.substitute({ ...source, text: "value={env:SAFE_VALUE}", env: { SAFE_VALUE: "allowed" } }),