Skip to content

runtime(standalone host): the unsupported-database-URL-scheme refusal prints the whole OS_DATABASE_URL, so a password in the URL reaches the console and the boot log #23764

Closed

Description

@objectstack-fleet

Filing gate ①, class (a), with the security exception (a credential in a boot log). reach: measured at the public door by #23633's dev (os-dev-report on #23633, out-of-scope finding 1), on PR #23761's branch, where packages/runtime is unchanged from main.

Filed by domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG, in the same act as the seat's review of PR #23761. ⛔ Not a claim. Grade and lane are triage's.

What was measured

OS_DATABASE_URL=redis://admin:PASSWORD@127.0.0.1:6399/0 os dev --no-watch over a plain config (the runtime's standalone host):

  • os dev's own Database: row redacts the credential;
  • then the boot refusal prints [StandaloneStack] Unsupported database URL scheme: redis://admin:PASSWORD@127.0.0.1:6399/0 in full.

Why, read on origin/main

detectDriverFromUrl (packages/runtime/src/standalone-stack.ts:399) builds its refusal with `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. …`, the raw URL.

The CLI host's refusal for the same URL, added by PR #23761 (#23633), names the scheme only, "never the URL, so credentials in the URL are not printed". After that PR lands, the two hosts refuse the same URLs but only one of them leaks.

Reader who acts

Triage grades it and routes it. The landing is detectDriverFromUrl's throw in packages/runtime/src/standalone-stack.ts: name the scheme (or a redacted URL, as redactConnectionUrl does for the banner), never the raw URL. Pin it with a credential-bearing URL whose password must not appear in the error.

Dedupe

MCP search_issues on objectstack-ai/objectstack, closed included: standalone-stack unsupported database URL scheme refusal prints password credentials detectDriverFromUrl gave 5 hits. The nearest is #8082 (closed; a [Decision] on URL-embedded credentials in driver config.url at publish). It is about stored config, not this refusal's text. None names this throw.

Dedupe words: unsupported database URL scheme prints password; standalone-stack refusal credential leak; detectDriverFromUrl full URL in error.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    First grade · domain:cli · p1 · security · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T01:03Z

    Read in this act: the body in full, 0 comments.

    ⛔ Not a claim.

  2. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 27 (#23764: the standalone host's unsupported-database-URL-scheme refusal names the scheme, never the raw OS_DATABASE_URL, so a password in the URL no longer reaches the console or the boot log) · 2026-10-16T01:58Z
    Session: session_01Q9PZ5Pzqcus4woVvTZwWNU
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23764-unsupported-scheme-no-url
    Worktree: objectstack-issue-23764
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    Scope: batch 1, by the maintainer's 「当前任务处理完,合并后就改为串行」. #23050 slice 7 landed at 2026-10-16T01:54Z and was released, so nothing of this seat's is in flight. This is the lane's only priority:p1 card in pm:queue, ahead of the two p2 cards (#23737, #23050), read at this act.
    Order, read at this act: triage's grade 6205637664 (p1, security) puts no order on it. PR #23761 (#23633, seat 1's) leaves packages/runtime unchanged, and no open PR touches packages/runtime/src/.
    File surface (read on origin/main ec44cf60bf; stop on a breach and explain it in the report):


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23764,
    "status": "done",
    "branch": "claude/issue-23764-unsupported-scheme-no-url",
    "pr": "#23791",
    "session": "session_01Q9PZ5Pzqcus4woVvTZwWNU (the dispatching PM session, the harness-stamped Claude-Session; this run is mode:subagent)",
    "premise_still_valid": true,
    "summary": "Premise held, measured on base ec44cf6. With OS_DATABASE_URL=redis://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN, all five public doors printed each placeholder once in the refusal: os dev --no-watch, os start, os serve, os migrate plan, and the stdout error field of os migrate recorded-by --json. detectDriverFromUrl (packages/runtime/src/standalone-stack.ts) now refuses with 'Unsupported database URL scheme "redis://"' and keeps its supported list unchanged. The scheme comes from a new urlSchemeOf helper, which uses the regex the arm already tested and so cannot hold a userinfo, host, path or query. Both siblings were measured to carry a credential and were changed the same way: the retired in-memory engine refusal (its explicit, env and declared-datasource phrases) and the sqlite non-path refusal (OS_DATABASE_DRIVER=sqlite beside a credentialed postgres:// URL). After the change, every door counts 0 for every placeholder. Every detection arm returns the same kind as before, and the URL handed to the driver keeps its credentials. Changeset: @objectstack/runtime patch, with FROM/TO.",
    "tests": "HEAD c61b136 (git rev-parse --short HEAD), all runs under os-verify-lock with OS_VERIFY_LOCK_SLOT=issue-23764. | New: packages/runtime/src/standalone-stack-url-refusal-credentials.test.ts, 30 cases: each refusal through each door (the databaseUrl option, OS_DATABASE_URL, a declared default datasource, databaseDriver and OS_DATABASE_DRIVER); a plain Error with no cause; names the scheme and the remedy; no own property of the error (message, stack) carries PLACEHOLDER-USER, PLACEHOLDER-SECRET or PLACEHOLDER-TOKEN. CONTROL: 16 detection-arm rows on the same kind, and 3 rows where a credentialed postgres/mysql/mongodb URL resolves with url byte-identical. standalone-stack.test.ts: the memory:// boot pin now expects the scheme and no URL tail, plus one new createStandaloneStack row over a credentialed redis:// URL. | pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/standalone-stack-url-refusal-credentials.test.ts src/standalone-stack.test.ts src/standalone-stack.libsql.test.ts src/standalone-stack.mysql.test.ts src/resolve-project-database.test.ts: Test Files 5 passed (5), Tests 155 passed (155), VERDICT command-exit 0. The first run was 10 red on a test-helper bug: a JSON-escaping positive control, fixed in 1d02ffc, not a product failure. | pnpm --filter @objectstack/runtime test (project local): Test Files 399 passed (399), Tests 5979 passed | 19 skipped (5998), VERDICT command-exit 0. | pnpm --filter @objectstack/runtime typecheck: exit 0. check:test-typecheck OK with 27 files / 190 errors held in the ledger, so the new test file adds no debt. tsc --listFiles -p tsconfig.test.json counts both changed test files (2). | Ablation, via node scripts/ablation-replace.mjs in wrap mode plus a shell trap restoring from HEAD with a blob-hash check. The subject is imported from src by relative path, so there is no dist leg. (1) Raw URL put back into the unsupported-scheme refusal, scheme wording kept: anchor 1 to 0, blob 787073f6c5 to cb57a854f7; 5 red (the 4 unsupported-scheme rows and the boot row, all on the placeholder assertion), 116 green (every memory, sqlite and CONTROL row, and the libsql and mysql suites); restored with blob equal to HEAD and git diff HEAD empty. (2) Env memory phrase: 1 red on the placeholder assertion, 63 green. The first attempt inserted the URL inside the asserted wording and went red on the wording assertion, so it was void and was redone. (3) sqlite non-path refusal: 2 red on the placeholder assertion, 62 green. Every leg was restored and verified. | Door readings with the built CLI and runtime and a plain defineStack fixture: before (standalone-stack.ts at base, runtime rebuilt) each door printed 1/1/1 of USER/SECRET/TOKEN; after, 0/0/0 on all five; exit 1 throughout. The runtime dist was rebuilt from HEAD afterwards (dist marker fixed=1, raw=0).",
    "gates": "Local, at c61b136: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 62 commands over 4 paths. All 62 were run, each exit captured before any pipe: 61 exit 0 on the first pass. pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: 9 packages without dist) and exited 0 on a re-run once those dist trees existed (110 entries / 68 packages / 722 CJS files). | --ran reconciliation (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE, with every line in the command :: exit N form): 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero. | Lint, narrowed: eslint --no-inline-config over the 3 changed .ts files, exit 0. Evidence: (1) they are the derived change set's lintable files; (2) --print-config shows 6 per-file syntactic rules and no typed-lint parserOptions.project; (3) positive control: an injected max-len 40 fires on all three (101/333/700). Full pnpm lint is left to CI's Lint and Repo Gates. | Not run locally: test:repo (no repo-project test changed, declared to CI); packages/cli suites (not touched); local-only check:dev-prereqs and check:skill-frame-freshness (no packages/spec and no frame files touched). | PR #23791 check-runs on c61b136, read once at report time: 23 runs. No other open PR may claim the same issue: success. Packed-tarball smoke (opt-in): skipped. In progress, not yet concluded (honest value, not awaited): Type Check · source gates, Type Check · consumer gates, Type Check · workspace, Type Check · debt ledger, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Lint · scoped · query-options erasure, Lint · CI, release & engine gates, Lint · repo & tooling gates, Flag docs affected by code changes, The card this PR closes must claim this branch, filter, Check PR Size, Governed Surface Queue Guard, No other open PR may claim the same single-writer path, Check Changeset, Part-of PR must not also close its card, Check Documentation Links. Test Core and Build Core had not yet appeared.",
    "mcp_calls": "0: no MCP GitHub tool was called (reads used gh api REST GETs; writes went through scripts/pm/with-fleet.sh --via dispatch)",
    "api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, each executed by fleet-write.yml as objectstack-fleet[bot]): (1) pr_create, which sent POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23791/assignees [sunhaolin], run 38841652804, success; read-back 11469 bytes sent and stored, identical; (2) comment, POST /repos//issues/23764/comments (this os-dev-report). Plus git push of the branch (not a REST write). label-write: 0 (none named by the dispatch, and skip-changeset does not apply). No claim post and no card-assignee write.",
    "open_questions": [
    {
    "question": "Mechanism (b) found a drivable door: packages/cli/test/migrate-exit-code.e2e.test.ts already spawns os migrate recorded-by --json over wat://nope. The pin list asks for a door-level row, but the ruling forbids editing packages/cli. Should a credential-absence door row be added in the CLI lane?",
    "options": [
    "A: No door row. The unit pins sit at the one throw every door reaches (measured: all 5 doors print that message and nothing else carrying the URL), the boot row covers createStandaloneStack, and the existing e2e keeps reading "Unsupported database URL scheme" from the --json payload.",
    "B: A CLI-lane follow-up adds a placeholder-credential row to migrate-exit-code.e2e.test.ts (about 4 to 9 s per spawn) that asserts the payload carries no placeholder."
    ],
    "recommendation": "A, because the door adds no second copy of the URL (measured 1/1/1 before, 0/0/0 after, no cause, no stack, no file), so a door pin would re-assert the unit pin through a slower path. Choose B only if the seat wants the leak class pinned end-to-end."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: exception: security, plus the public door measured with the built CLI at c61b136: OS_MODE=off OS_DATABASE_URL=memory://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN os serve exits 1, printing the whole URL in "OS_DATABASE_URL "memory://PLACEHOLDER-USER:PLACEHOLDER-SECRET@..." selects the in-memory (mingo) engine", each placeholder once · evidence: packages/cli/src/utils/storage-driver.ts:279 on ec44cf6, the inferDriverTypeFromUrl memory arm, interpolates the raw value u into retiredMemoryEngineMessage. PR #23761 rewrites this function and leaves this arm unchanged (its new unknown-scheme arm names the scheme only). This is the CLI-host sibling of this card, in packages/cli, which this card does not edit · dedupe words: CLI memory scheme refusal prints OS_DATABASE_URL password; inferDriverTypeFromUrl memory:// full URL in error; os serve OS_MODE=off memory URL credential",
    "class: a · reach: public door measured with the built CLI at c61b136: OS_DATABASE_URL=FILE:./urd.db os serve in a plain project created a directory literally named "FILE:." in the working directory, holding a 708 KB SQLite file urd.db; lower-case file:./urd.db opens ./urd.db. The boot then stopped on an unrelated production prerequisite (OS_SECRET_KEY) · evidence: packages/runtime/src/standalone-stack.ts, where detectDriverFromUrl matches file: case-insensitively but sqliteFilenameFromUrl strips only a lower-case file: prefix; API reading: resolveStandaloneDatabase({ databaseUrl: "FILE:/tmp/x.db" }).sqliteFile is "FILE:/tmp/x.db". This is a different defect class from this card, so it was not fixed here · dedupe words: upper-case FILE: database URL; sqliteFilenameFromUrl case-sensitive file prefix; FILE: directory created in working directory",
    "carrier: none · noted, not filed: unsupportedDriverMessage (standalone-stack.ts) echoes the raw OS_DATABASE_DRIVER value. That is a driver spelling by contract and carries a URL only if an operator writes one into the driver variable. It is listed in the PR's sibling table, not changed",
    "carrier: none · noted, not filed: the resolve-project-database.ts docstring of retiredMemoryEngineMessage still shows The database URL "memory://" as an example selection phrase. It is a comment outside the card's file surface and is in the PR's Acceptance notes"
    ],
    "deviations": [
    "The pins spell the username PLACEHOLDER-USER, not the dispatch's literal user:PLACEHOLDER-SECRET@, so the username-absence assertion cannot collide with an ordinary word in a message.",
    "No door-level pin: its home is packages/cli, which the ruling holds off. The doors were measured once, before and after, instead (open_questions[0]).",
    "Lint: the dispatch asks for pnpm lint or the proven narrowing, and the role file assigns repo-wide scans to CI. The narrowing, with its three pieces of evidence, satisfies both. Stated here rather than chosen silently.",
    "The memory phrases are reworded ("A database URL with the "memory://" scheme", "An OS_DATABASE_URL with the …", "URL scheme "memory://""), not only substituted, so they do not claim the URL was exactly the scheme. The sqlite refusal moved from ": "URL"" to "(its scheme is …)" / "(it names no file after "file:")". The existing memory:// boot pin was updated from the whole URL to the scheme.",
    "The first memory ablation attempt was void (it tripped the wording assertion) and was redone.",
    "check:dual-build-cjs-loads: first pass exit 3 (prerequisite), then re-run exit 0. Recorded as exit 0 in --ran, with both runs reported.",
    "Commit trailers use the model-free AGENTS.md pair (Claude-Session plus Co-authored-by: Claude). The harness reminder's model-named Co-Authored-By was not used, per AGENTS.md's no-model-identifier rule."
    ],
    "files_changed": [
    "packages/runtime/src/standalone-stack.ts",
    "packages/runtime/src/standalone-stack-url-refusal-credentials.test.ts (new)",
    "packages/runtime/src/standalone-stack.test.ts",
    ".changeset/23764-unsupported-scheme-no-url.md (new; @objectstack/runtime patch, Clause-②: no)"
    ],
    "reading_a": "Hypothesis (a) holds, and there is no second copy. Before the change, each of the 5 doors printed each placeholder exactly once: the refusal line. os dev and os start's Database: row already redacts (redis://127.0.0.1:6399/0). No door printed a stack, the error carries no cause, and no file written under OS_HOME or the project held a placeholder. After the change, every door counts 0/0/0. The pins also serialise every own property of the thrown error.",
    "reading_b": "Hypothesis (b) holds and is wider than stated. Five public doors reach the throw through the standalone host: os dev --no-watch, os start, os serve (each over a plain defineStack config), os migrate plan, and os migrate recorded-by --json (in the stdout JSON error field). The doors are drivable (packages/cli/test/migrate-exit-code.e2e.test.ts already spawns the last one), but a door pin would live in packages/cli, which is held off. See open_questions[0].",
    "sibling_messages": [
    "detectDriverFromUrl unsupported-scheme refusal → yes (any scheme://user:password@…?token=…) → fixed (the card)",
    "refuseRetiredMemoryEngine explicit/env/declared-datasource phrases (:540-543) → yes (memory:// and mingo:// accept anything after //; measured echo of all three placeholders via databaseUrl and OS_DATABASE_URL, and via a declared datasource whose url is memory://…) → fixed",
    "sqliteFilenameFromUrl non-path refusal (
    :557) → yes (OS_DATABASE_DRIVER=sqlite or databaseDriver: sqlite beside a credentialed postgres:// URL echoed all three placeholders) → fixed",
    "unsupportedDriverMessage (:178, OS_DATABASE_DRIVER value) → not a URL by contract; carries one only if an operator puts a URL in the driver variable → listed, not changed",
    "assertUrlNamedForRemoteDriver (:505) → no (driver kind only) → n/a",
    "legacy-file notice console.warn (:707) → no (state-directory file path) → n/a",
    "unreachable-kind throw (:801) → no (kind only) → n/a"
    ],
    "before_after": [
    "redis://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN → before: "[StandaloneStack] Unsupported database URL scheme: redis://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:6399/0?authToken=PLACEHOLDER-TOKEN. Supported schemes: …" → after: "[StandaloneStack] Unsupported database URL scheme "redis://". Supported schemes: …"",
    "https://PLACEHOLDER-USER:PLACEHOLDER-SECRET@… (non-Turso) → before: the whole URL → after: Unsupported database URL scheme "https://"",
    "memory://PLACEHOLDER-USER:… (databaseUrl) → before: The database URL "memory://PLACEHOLDER-USER:PLACEHOLDER-SECRET@…" → after: A database URL with the "memory://" scheme (mingo:// likewise)",
    "memory://PLACEHOLDER-USER:… (OS_DATABASE_URL) → before: OS_DATABASE_URL "memory://PLACEHOLDER-USER:PLACEHOLDER-SECRET@…" → after: An OS_DATABASE_URL with the "memory://" scheme",
    "declared default datasource url memory://… → before: (driver: memory, URL "memory://…") → after: (driver: memory, URL scheme "memory://")",
    "postgres://PLACEHOLDER-USER:… with OS_DATABASE_DRIVER=sqlite → before: sqlite driver was selected but the URL does not look like a file path: "postgres://PLACEHOLDER-USER:PLACEHOLDER-SECRET@…" → after: … does not look like a file path (its scheme is "postgres://")",
    "file: → before: … does not look like a file path: "file:" → after: … does not look like a file path (it names no file after "file:")"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23791 comment 6207095188 (no contract review owed) · domain:cli seat 2 · 2026-10-16T03:04Z


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23791 → 9d4fb3c306 · 2026-10-16T03:37Z

    domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU · holder of claim 6206304815, released by this landing. Review: ACCEPT 6207095188 on the PR (no contract review owed).

    Reading Value
    PR merged: true, merged_at 2026-10-16T03:36Z, through the merge queue (added_to_merge_queue 2026-10-16T03:07Z)
    Queue branch gh-readonly-queue/main/pr-23791-* absent (git ls-remote, 0 refs)
    origin/main 9d4fb3c306 is an ancestor; one parent (609d07a7df); 4 files, +285 / −8, as reviewed at c61b1366e1
    Closing keywords in the landed commit Fixes #23764 only
    Lane reconciliation 15 open domain:cli cards (read at this stamp), #23764 no longer among them

    State change: #23764 closed completed by the merge; pm:dispatched removed in this act. The standalone host's database-URL refusals (an unsupported scheme, the retired in-memory engine's three phrases, and the sqlite non-path refusal) now name the URL's scheme and never print the URL. So a password or token in OS_DATABASE_URL no longer reaches the console or the boot log at os dev, os start, os serve or os migrate. Which URLs resolve, and the URL handed to the driver, are unchanged.

    Filed from this card's report: #23792 (the CLI host's own memory:// refusal still prints the whole URL; the CLI-host sibling of this card) and #23794 (an upper-case FILE: URL creates a FILE:. directory).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions