VYPR
Medium severity5.5NVD Advisory· Published May 18, 2026· Updated May 19, 2026

CVE-2026-45246

CVE-2026-45246

Description

Summarize prior to 0.15.1 contains an insecure file permission vulnerability in the refresh-free configuration rewrite path that allows local users to read sensitive credentials by exploiting default filesystem permissions. When the refresh-free path rewrites the configuration file, it creates the replacement with default process umask permissions instead of preserving the original file permissions, exposing the config file containing API keys and provider credentials to other local users on shared Unix-like systems.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Summarize before 0.15.1 exposes API keys in config.json to other local users due to insecure file permissions during refresh-free rewrites.

Vulnerability

In Summarize versions prior to 0.15.1, the refresh-free configuration rewrite path does not preserve original file permissions when updating ~/.summarize/config.json. Instead, the replacement file is created with default process umask permissions, which on shared Unix-like systems can expose the configuration file containing sensitive API keys and provider credentials to other local users. The vulnerability is present in the refresh-free code path that rewrites the config file after a model refresh [1][2][4].

Exploitation

An attacker must be a local user on the same Unix-like machine as the victim. The victim must run the summarize refresh-free command while the config file contains sensitive credentials (e.g., in the env or apiKeys sections). The refresh-free update then creates the temporary replacement config file with default permissions (e.g., 0644 under a typical 022 umask) and renames it over the original file, widening permissions. No other authentication or user interaction beyond the victim running the command is required [1][4].

Impact

Successful exploitation allows a local unprivileged attacker to read the victim's ~/.summarize/config.json, thereby disclosing any stored API keys, provider environment values, or other credentials. This can lead to unauthorized access to third-party services the victim uses (e.g., OpenAI, OpenRouter). The disclosure is limited to local access, but on shared or multi-user systems this represents a medium severity information disclosure [1][4].

Mitigation

The vulnerability is fixed in Summarize version 0.15.2, released on 2026-05-17 [3]. The fix ensures that the ~/.summarize directory is created with mode 0700, the replacement config file is written with mode 0600, and after rename the final config.json is repaired to 0600. Users should upgrade to version 0.15.2 or later. As a workaround, users on shared systems can manually restrict permissions on the config file and directory after each refresh-free command. The vulnerability is not known to be listed in CISA KEV [1][2][3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
9e990193650a

[security] fix(refresh-free): keep config rewrites private (#217)

https://github.com/steipete/summarizeHinotobiMay 14, 2026via nvd-ref
2 files changed · +89 3
  • src/refresh-free.ts+6 3 modified
    @@ -1,4 +1,4 @@
    -import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
    +import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
     import { homedir } from "node:os";
     import { dirname, join } from "node:path";
     import JSON5 from "json5";
    @@ -747,11 +747,14 @@ export async function refreshFree({
         root.model = "free";
       }
     
    -  await mkdir(dirname(configPath), { recursive: true });
    +  const configDir = dirname(configPath);
    +  await mkdir(configDir, { recursive: true, mode: 0o700 });
    +  await chmod(configDir, 0o700).catch(() => {});
       const next = `${JSON.stringify(root, null, 2)}\n`;
       const tmp = `${configPath}.tmp-${process.pid}-${Date.now()}`;
    -  await writeFile(tmp, next, "utf8");
    +  await writeFile(tmp, next, { encoding: "utf8", mode: 0o600 });
       await rename(tmp, configPath);
    +  await chmod(configPath, 0o600).catch(() => {});
     
       stdout.write(`Wrote ${configPath} (models.free)\n`);
     
    
  • tests/refresh-free.permissions.test.ts+83 0 added
    @@ -0,0 +1,83 @@
    +import { mkdirSync, mkdtempSync, statSync, writeFileSync } from "node:fs";
    +import { tmpdir } from "node:os";
    +import { join } from "node:path";
    +import { Writable } from "node:stream";
    +import { afterEach, describe, expect, it, vi } from "vitest";
    +import { refreshFree } from "../src/refresh-free.js";
    +
    +const mocks = vi.hoisted(() => ({
    +  generateTextWithModelId: vi.fn(async () => ({
    +    text: "OK",
    +    model: "openrouter/test/free:free",
    +    usage: null,
    +    raw: null,
    +  })),
    +}));
    +
    +vi.mock("../src/llm/generate-text.js", () => ({
    +  generateTextWithModelId: mocks.generateTextWithModelId,
    +}));
    +
    +function sink() {
    +  return new Writable({
    +    write(_chunk, _encoding, callback) {
    +      callback();
    +    },
    +  });
    +}
    +
    +describe("refresh-free config file permissions proof", () => {
    +  afterEach(() => vi.restoreAllMocks());
    +
    +  it("keeps refresh-free config rewrites owner-only", async () => {
    +    const oldUmask = process.umask(0o022);
    +    try {
    +      const home = mkdtempSync(join(tmpdir(), "summarize-refresh-free-perms-"));
    +      const configDir = join(home, ".summarize");
    +      const configPath = join(configDir, "config.json");
    +      mkdirSync(configDir, { recursive: true, mode: 0o700 });
    +      writeFileSync(
    +        configPath,
    +        JSON.stringify({ env: { OPENAI_API_KEY: "sk-secret" }, models: {} }, null, 2),
    +        { encoding: "utf8", mode: 0o600 },
    +      );
    +      expect(statSync(configPath).mode & 0o777).toBe(0o600);
    +
    +      const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
    +        const url = String(input);
    +        if (url === "https://openrouter.ai/api/v1/models") {
    +          return new Response(
    +            JSON.stringify({
    +              data: [
    +                {
    +                  id: "test/free:free",
    +                  name: "Test Free 70B",
    +                  context_length: 128000,
    +                  top_provider: { max_completion_tokens: 4096 },
    +                  supported_parameters: ["temperature"],
    +                  architecture: { modality: "text->text" },
    +                  created: Math.floor(Date.now() / 1000),
    +                },
    +              ],
    +            }),
    +            { status: 200, headers: { "content-type": "application/json" } },
    +          );
    +        }
    +        throw new Error(`unexpected fetch: ${url}`);
    +      }) as unknown as typeof fetch;
    +
    +      await refreshFree({
    +        env: { HOME: home, OPENROUTER_API_KEY: "or-secret" },
    +        fetchImpl,
    +        stdout: sink(),
    +        stderr: sink(),
    +        options: { runs: 0, smart: 0, maxCandidates: 1, timeoutMs: 1, minParamB: 0 },
    +      });
    +
    +      expect(statSync(configPath).mode & 0o777).toBe(0o600);
    +      expect(statSync(configDir).mode & 0o777).toBe(0o700);
    +    } finally {
    +      process.umask(oldUmask);
    +    }
    +  });
    +});
    

Vulnerability mechanics

Root cause

"The refresh-free configuration rewrite path creates the replacement config file using the default process umask permissions instead of preserving or enforcing owner-only permissions, causing the file to inherit overly permissive access rights."

Attack vector

An attacker with local user access on a shared Unix-like system can read the configuration file after a refresh-free rewrite occurs. The vulnerability is triggered automatically when the refresh-free path rewrites the config file; the replacement file is created with permissions derived from the process umask (e.g., 0o644) rather than the original owner-only mode (0o600). Because the config directory is also created without explicit restrictive permissions, the entire config path may be readable by other local users. The attacker does not need any special privileges beyond a local shell account on the same machine [CWE-732].

Affected code

The vulnerable code is in `src/refresh-free.ts` in the `refreshFree` function. The `mkdir` call for the config directory lacked a `mode` argument, and the `writeFile` call for the temporary replacement file used only `"utf8"` encoding without a `mode` parameter, causing both to inherit the process umask. No permission enforcement was applied after creation.

What the fix does

The patch adds explicit permission enforcement at three points in the refresh-free config rewrite path [patch_id=424355]. First, `mkdir` for the config directory now passes `mode: 0o700` to ensure the directory is owner-only. Second, `writeFile` for the temporary replacement file uses `mode: 0o600` so the file is created with owner-read/write only. Third, explicit `chmod(0o700)` and `chmod(0o600)` calls on the directory and file respectively act as a defense-in-depth measure in case the initial mode flags are not honored by the underlying filesystem. Together these changes ensure that sensitive credentials in the config file are never exposed to other local users.

Preconditions

  • authAttacker must have a local user account on the same Unix-like system as the victim.
  • inputThe refresh-free config rewrite must be triggered (e.g., by running summarize with refresh-free options).
  • configThe original config file must contain sensitive credentials (API keys, provider tokens) and must have been created with owner-only permissions that the rewrite then fails to preserve.

Reproduction

No public PoC with explicit reproduction steps is included in the bundle beyond the test file. The test at `tests/refresh-free.permissions.test.ts` demonstrates the expected behavior: it creates a config directory with mode 0o700 and a config file with mode 0o600, runs `refreshFree`, and asserts that both the directory and file retain owner-only permissions after the rewrite. To reproduce the vulnerability, one would run the same setup without the patch and observe that the rewritten config file inherits the process umask (e.g., 0o644) instead of 0o600.

Generated on May 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.