Repository navigation
[Decision] URL-embedded credentials (user:password@host in driver config.url) remain a live cleartext door after #7990 — refuse at publish, or accept as residual risk? #8082
Description
Escalated from PR #8078's dev report (#7990 comment 5268367568, open question 1) by the domain:spec PM seat (session session_0123k4cam2jEAkPmbJeoaY3r). On the manual floor: refusing URL userinfo is an accept-set change to a published authoring surface, and the legitimate-DSN-workflow carve-out is a product call.
Background
#7990's ruled Option A (PR #8078) refuses inline config.password / config.authToken on all four SQL/NoSQL driver config schemas, diverting authors to the secret binder / external.credentialsRef. Measured during that implementation and pinned as a FACT (in driver-credential-refusal.test.ts, deliberately asserting today's acceptance): config.url admits postgresql://user:password@host/db on all four drivers, and that URL lands in sys_metadata cleartext exactly as config.password did. The refused key has a one-syntax-over workaround, and the schema prose even documents the URL's password segment.
Premise re-check commands
git grep -n "user\[:password\]" origin/main -- packages/spec/src/data/driver/postgres.zod.ts(the documented URL format)- The pinned acceptance test:
git grep -n "url" origin/main -- packages/spec/src/data/driver/driver-credential-refusal.test.ts(post-feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990, spec half) #8078 merge)
Question
Should publish-time validation refuse a driver config.url carrying userinfo credentials?
Options
- A — refuse URL userinfo at publish (value-level parse on
config.url; guidance: move the password to the binder /credentialsRef, keep the URL credential-free). Closes the workaround loudly.⚠️ Cost: DSN-style workflows (OS_DATABASE_URL-shaped single-string configs) are a common ops pattern; authored-metadata refusal must not break URLs that arrive via runtime environment rather than authored metadata — the carve-out line needs your word. Note (measured, feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990, spec half) #8078):${…}placeholders in authored metadata resolve to nothing — they reach the client verbatim — so "put a placeholder in the URL" is not a working escape today. - B — accept as documented residual risk: keep the pin, document the door in the security posture docs, revisit if measured in the wild.
- C — fold into the services half (The 400 that four comments cite as the
$searchFieldshalf of the__searchcontract is asserted by no test #8080-scope): scrub userinfo at the write door instead of the contract (value rewriting or refusal atdatasource-admin), leaving the spec accept-set unchanged.
Four-lens
- 实际业务需求 — the door is live and measured on all four drivers; it is the exact sink the maintainer just ruled worth closing, one syntax over. But no in-repo or reported author writes credentials in authored URLs today (census found none), so the pull is preventive, not remedial.
- 项目长远合理性 — leaving the workaround open makes the [security]
sys_metadata.metadatais a general cleartext sink: any authored artefact whose schema permits an inline credential lands it there (datasourceconfig.password, connectorauthentication) #7990 closure structurally incomplete: the next refused author (or AI) gets steered to the URL form by the refusal itself. A publish-time value check is one rule at the same contract; a write-door scrub (C) is a second, drifting copy of the policy. - 防 AI 写错 — strongest is A: an AI author refused on
config.passwordwill otherwise "fix" the error by moving the secret into the URL — silently succeeding, which is the masked-failure shape this programme exists to kill. The refusal message can teach the correct split (URL without userinfo + ref for the secret). - 创业阶段收缩 — A is bounded (one value-level check shared by four schemas, guidance text reused). Its only real cost is the DSN carve-out decision, which is precisely the part that cannot be a dev default.
Recommendation
A, premised on your ruling of the carve-out: authored metadata refuses URL userinfo; runtime-environment-provided URLs (not authored, never stored in sys_metadata) stay out of the contract's reach. If you would rather not spend the carve-out decision now, B with the pin kept is honest and cheap — ⛔ not C (a second policy copy at a different layer).
Refs: #7990 (ruling + report), PR #8078 (the pinned fact), #8075 (further census).
Activity
hotlong commentedon Aug 12, 2026
Maintainer ruling — 2026-08-12 (live PM chat,
session_01GxKQfv3k8b6a2d2QrZU411; maintainer sam, verbatim: 「接受你的全部建议。」 accepting the decision-box analysis in full). Moved out of the decision box →pm:queue.Ruling: option A — refuse URL userinfo at publish, via a value-level parse shared by the four driver schemas. Carve-out line: runtime-environment DSNs do not pass through the publish door, so they are unaffected by construction — state that explicitly in the refusal's docs rather than leaving it implied. ⛔ Option C (scrub at the services write door) stays rejected — a second, drifting copy of the policy. Option B (document as residual risk) was honest but loses口径 consistency with #7990's per-artefact contract closure.
Ruled together with #8103 under one template — close the forward door, then remediate existing material by explicit action. Here the forward door is the publish refusal; #8126 (
37b82ed) already narrowed the read exposure today, so no separate remediation sweep is called for on this card.${…}placeholders reach the client verbatim, so "use a placeholder instead" does not work today — a refusal message that steers authors (or an AI) toward a broken escape recreates the masked-failure shape this programme exists to kill. The acceptance pin atdriver-credential-refusal.test.ts:166-171(recording today's acceptance as a FACT) must be inverted to a rejection pin assertingcode+status— not deleted. Spec semantic lane ⇒claude-fable-5. Unblocks progress on #7990's parked boundary-guard trigger; #8075's census feeds it.Generated by Claude Code
hotlong commentedon Aug 13, 2026
Claim: PM loop round 1 (expanded batch — maintainer 2026-08-13: 「任务很多,加速处理」)
Session:
session_01Euoy6wyfzgiWtgCg4s6JK2Branch:
claude/issue-8082-url-userinfo-publish-refusalWorktree:
objectstack-issue-8082Domain:
domain:specFile surface:
packages/spec/src/data/driver/**(the four driver config schemas + a shared value-level URL parse),packages/spec/src/data/driver/driver-credential-refusal.test.ts(acceptance pin at :166-171 inverted to a rejection pin, ⛔ not deleted),packages/spec/src/data/datasource.zod.tsonly if the shared parse hooks there; changeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: claude-fable-5(mandatory clause — narrows the accept set)Serial constraints cleared: no in-flight claim touches
packages/spec/src/data/driver/**(in-flight this lane: #8010 →ui/view.zod.ts, #8124 →api/errors.zod.ts+ types + rest, #7917 → read-only; surface seat #8201/#8202 →ui/+shared/editability+data/field.zod.tsSelectOption block only). Predecessor PRs on this surface all merged: #8078 (#7990 spec half), #8126 (37b82ed). Premise re-verified onorigin/maintoday:driver-credential-refusal.test.tsexists at the claimed path; maintainer ruling (option A) recorded on-card 2026-08-12.Generated by Claude Code
test(spec): re-spell two driver URI fixtures credential-free (#8082 f…
hotlong commentedon Aug 13, 2026
{ "issue": 8082, "status": "done", "branch": "claude/issue-8082-url-userinfo-publish-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/8341", "premise_still_valid": true, "summary": "Ruled Option A implemented: one shared value-level parse (urlUserinfoPassword/credentialFreeUrl in packages/spec/src/data/driver/common.zod.ts) refuses a non-empty userinfo password on postgres/mysql/mongo config.url and turso config.url + config.syncUrl (syncUrl is a small same-mechanism extension beyond the four url keys, flagged in the PR); bare-user userinfo stays accepted, following the #7990 posture (username is writable, only the secret is refused) — which also keeps the #8126 legacy-row redacted round-trip green. The #8078 acceptance pin is inverted to a rejection pin asserting the zod issue code ('custom') + config.url path + message contract; an HTTP status does not exist at the schema layer (the publish door wraps every schema refusal uniformly as metadata-protocol's 422 INVALID_METADATA), so that half of the code+status clause is stated honestly rather than fabricated. Two files outside the dispatched surface were edited under the fixture-sweep clause and are flagged in the PR: service-datasource's #8126 pin that was written to flip on exactly this ruling, plus the redaction module's now-stale 'unruled' doc paragraph. Refusal message names binder/credentialsRef, states the runtime-DSN carve-out explicitly, and warns placeholders resolve to nothing instead of steering to that broken escape; ADR-0087 D3 semantic entry datasource-config-url-userinfo-refused registered; changeset major (rc pre-mode).", "tests": "spec full suite: 10288 passed, 2 failed (mongo/postgres 'accept config with connection URI' fixtures spelling userinfo URLs — disposition re-spell; my static sweep missed them, the test run caught them) → after re-spell src/data/driver: 'Test Files 9 passed / Tests 272 passed', re-confirmed on the merged tree. spec typecheck green (check:test-typecheck OK). Consumers (measured importers): service-datasource 'Test Files 16 passed / Tests 376 passed' incl. the inverted write-door pin + tsc --noEmit clean; driver-turso 966 passed; downstream-contract 20 passed. Gates: check:generated green after --fix of exactly the 5 proved-stale artifacts (spec-changes, upgrade-guide, api-surface, export-origins, reference docs); check:driver-conformance 'OK — 45 covered cells'; check:nul-bytes OK; check:adr-0087-registration recognizes the registered marker. Reverse verification from the committed state: reverting the four schema files to origin/main flipped exactly the predicted set — 26 red (5 rejection cases x 5 URL surfaces + the inverted DatasourceSchema pin), 44 green (detector, acceptance, #7990 pins) — then restored byte-identical from the commit (git status clean vs HEAD). Merged origin/main via scripts/pm/os-regen-merge.sh (merge committed first, regeneration as its own commit, gen:openapi restored); CI on PR #8341 not awaited per the report-at-draft-time contract.", "open_questions": [], "out_of_scope_findings": [ "filed as #8336: ${...} placeholders in authored metadata resolve to nothing and reach the consumer verbatim — the broken escape two refusal messages now have to warn about (deduped: no existing card; named as file-or-file-out in the #8082 ruling)", "filed as #8337: turso ?authToken= in an authored config.url/syncUrl query string is credential material the userinfo refusal does not cover (at-rest cleartext certain; connect half unmeasured; extension deliberately not smuggled into the ruled accept-set change)" ] }Generated by Claude Code
hotlong commentedon Aug 13, 2026
ACCEPT — reviewed by the dispatching seat (session
session_01Euoy6wyfzgiWtgCg4s6JK2) against PR #8341's diff.Verified: 20 files; the shared value-level parse is one implementation wired into all five measured URL surfaces — the
turso.syncUrlextension beyond the ruling's literal "four" is the same authored-URL secret sink and closing four of five doors would have left the workaround the ruling template exists to kill, so it is accepted as within the ruling's intent (flagged, not smuggled). Bare-user posture follows #7990 (only the secret refused) and keeps the #8126 legacy round-trip green — the exact regression shape #7990's history warns about. The acceptance pin is inverted, not deleted, assertingcode+ the full publish-door envelope; the "no HTTP status exists at the schema layer" reading is honest and the 422INVALID_METADATAwrapper is the right envelope half. The two service-datasource files outside the declared surface are the fixture-sweep clause working as designed (a pin written to flip on exactly this ruling). ADR-0087 D3-without-D2 is correctly argued (a credential cannot be mechanically migrated). Reverse verification: 26 red / 44 green, direction predicted first. Findings #8336 (placeholder escape — the ruling's named file-out route) and #8337 (turso?authToken=query door) verified on GitHub, both correctly finding-grade and unassigned.Landing: draft until gate jobs conclude; flip + auto-merge (squash) on the timed check. On merge, #7990's parked boundary-guard trigger gains its unblock — recorded for the unlock sweep (#7990 stays
pm:blockedon #8081's services half).Generated by Claude Code
chore(spec): regenerate the ADR-0087 projections after merging main a…
fix(security): a bound credentialsRef reaches the postgres server on …
feat(spec)!: refuse URL-embedded credentials (user:password@host) in …
refactor(plugin-audit)!: retire `restore` from the sys_audit_log acti…
feat(spec): refuse `${…}` placeholder syntax in connection-material d…
fix(service-datasource): a bound credentialsRef reaches the mysql cli…
feat(spec): refuse credential-bearing URL query parameters (?authToke…
fix(service-datasource): inject a bound secret on the mongodb DSN bra…
fix(security): a bound credentialsRef reaches the postgres SERVER on …
docs(spec): re-anchor the last six dead tracker citations in packages…
test(spec): the last data/ file group's test titles state each cited …