Repository navigation
Commit cfd237f
feat(spec)!: a URL is judged as a URL parser cleans it — surrounding whitespace trimmed, tab and newline characters removed — with one userinfo grammar at both doors; a credential query parameter is refused in any string of a connector entry (#29875)
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no connector entry that `skills/**`, the
hand-written `content/docs/**`, `examples/**` or `packages/apps/**`
author holds a credential query parameter in any string under
`providerConfig` or `auth`, and none of their datasource or connector
URLs carries a credential part (scanned: 0 connector or datasource
literals affected; the shipped OpenAPI documents those apps reference by
path: 0), so nothing they teach changes; each refusal carries its own
prescription.
## 维护者速读
- 改了什么:连接器实例与数据源驱动配置里的值按 URL 解析器的读法判定:先去掉首尾的 C0
控制字符与空格,并删除值中任意位置的制表符、换行符与回车符,再判定——userinfo 与查询参数两半、连接器与数据源两扇门都如此;连接器的
userinfo 改用数据源已有的同一套语法(权限部分以 `scheme://` 或无 scheme 的 `//` 起始);连接器条目
`providerConfig` 与 `auth` 下任意字符串里带值的凭据类查询参数一律拒绝。保存门 422
`INVALID_METADATA`,定位到该字符串;两侧读取在写入判定的原始位置改写,其余字符原样返回。
- 为什么改:维护者裁决 D:两扇门一条值级规则,目的是让检查看到的就是使用方解析器看到的。删除制表/换行字符与连接器沿用数据源的 scheme
可选语法,正是这同一条规则;二者超出裁决的字面文本,席位已作为否决窗口呈报维护者(经契约复核档审核为既裁执行)。
- 风险与回滚:破坏性收窄(spec minor,一条 ADR-0087 D3 条目
`url-credential-judged-trimmed-and-in-any-connector-string`)。升级后在校验、构建产物加载与保存时被拒的有:连接器
`providerConfig`/`auth` 的任意字符串里带值的凭据查询参数;按解析器清洗后(去首尾空白、删除制表/换行字符)含凭据部分的
URL;连接器中无 scheme 的 `//` 权限部分所带的 userinfo。仓内 examples/apps/skills/docs 实测
0 处受影响。已存行不重判;读取隐去凭据部分,原样保存保留库值。回滚即 revert 本 PR。
-
先看:`packages/spec/src/data/driver/url-boundary.ts`(唯一的边界解析:按解析器清洗并保留到原始串的偏移映射,userinfo
区间供两扇门共用)、`packages/spec/src/integration/connector-url-credentials.ts`(`connectorUrlWithoutUserinfo`
同时供写判与读改写;查询参数判定作用于任意字符串)、`packages/spec/src/data/datasource-credential-redaction.ts`(`redactUrlPassword`
按写入侧同一区间改写)。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run
src/integration/connector-entry-url-credentials.test.ts
src/data/datasource-credential-redaction.test.ts`,预期全部通过。
- 需要你做的:无,席位落地。
## What changes
One value-level rule at both doors — **a URL is judged after trimming
surrounding whitespace; a credential query parameter is refused in any
string of the entry.**
- **Cleaned as a URL parser cleans it.** A value is judged after
trimming its leading and trailing C0 control characters and spaces and
removing every tab, newline and carriage return in it — both halves,
both doors. Each query pair is cleaned before its name is read. The
userinfo is read on the cleaned string and rewritten at the matching
offsets of the authored string.
- **One userinfo grammar at both doors.** The connector entry locates a
userinfo with `urlUserinfoSpan`, the scheme-optional grammar the
datasource doors already use: an authority starts at `scheme://` or a
scheme-relative `//`.
- **Any string.** The connector entry's credential query parameter rule
runs the single value-level parse the datasource doors already run
(`urlCredentialQueryParams`) on every string under `providerConfig` and
`auth`, scheme or none. Same names, same located refusal, never echoing
the value.
- **The read rewrites exactly what the write refuses.**
`packages/spec/src/data/driver/url-boundary.ts` (internal, not
re-exported) holds the one boundary parse: `urlTrimmedBounds` and
`urlUserinfoSpan`, offsets into the authored string. The datasource
write accessors (`urlUserinfoPassword`, `urlUserinfoUsername`) read the
userinfo there and the datasource read (`redactUrlPassword`) rewrites
the password there. On the connector side `connectorUrlWithoutUserinfo`
is both the write door's userinfo judgement (does it change the value?)
and the read's rewrite, and the query pairs go through the shared
`credentialQueryParamOf`. Every other character, the surrounding ones
included, is served as stored. At this head the boundary parse is
`urlAsParsed` (the cleaned string with a map back to raw offsets) and
`urlUserinfoSpan` (raw offsets, the cleaned userinfo, the mapped colon);
`connectorUrlWithoutUserinfo` removes the whole userinfo and its `@` at
those offsets.
- **Unchanged.** No rule keyed on a provider key name; `ConnectorSchema`
keeps its posture; header and environment maps are judged by neither
door; stored rows are never re-judged, a save of the served form keeps
the stored value, and an edit stores the edit. No public export added or
removed (`check:api-surface` unchanged).
- ADR-0087 D3 entry
`18.url-credential-judged-trimmed-and-in-any-connector-string.ts`;
changeset `@objectstack/spec` minor, BREAKING, with its FROM → TO and
the registration marker.
## Tests
-
`packages/spec/src/integration/connector-entry-url-credentials.test.ts`
— new block: refused at the string (userinfo behind surrounding
whitespace, `api_key`/`client_secret` in a URL behind whitespace,
`token` and the instance's `paramName` in a string with no scheme,
`signature` in a string that is not a URL, an `auth.tokenUrl` behind
whitespace); CONTROL accepts (clean URLs with surrounding whitespace,
scheme-less and non-URL strings holding none, empty credential values);
the read rewrites the same position with surrounding bytes kept and the
served form passes the write door; refused ⇔ rewritten over
`providerConfig.baseUrl`, a non-URL `providerConfig` string and
`auth.tokenUrl`.
- `packages/spec/src/integration/connector-credential-redaction.test.ts`
— the pin that held a non-URL string with a credential parameter
untouched pinned the removed boundary; it is split into a
still-untouched CONTROL (a non-URL string holding none) and the new
rewrite pin.
- `packages/spec/src/data/driver/driver-credential-refusal.test.ts` —
per URL-bearing driver key (postgres, mysql, mongo, turso `url`, turso
`syncUrl`): `user:password@` refused behind each of five surroundings,
and a bare username behind each not refused for a password;
`urlUserinfoPassword` judges the trimmed string.
- `packages/spec/src/data/datasource-credential-redaction.test.ts` — the
datasource read rewrites the password at the position the write door
judges, keeps the username and every surrounding character, and the
served form passes the write door; CONTROL strings come back byte for
byte; `redactDatasourceConfig` names `url`.
- Dogfood, both HTTP save doors:
`connector-meta-door-url-and-nested-keys.dogfood.test.ts` gains a block
on the existing showcase boot (five 422 refusals at `PUT
/api/v1/meta/connector/:name` with nothing stored; a credential-free URL
with surrounding whitespace saves, materializes `ready` and dispatches;
a seeded row is served with the parts removed at the judged position to
a member and an administrator; a save of the served form keeps the
stored values). New
`datasource-meta-door-url-credential-trimmed.dogfood.test.ts` (three 422
refusals at `PUT /api/v1/meta/datasource/:name` with nothing stored; a
bare username and a clean URL save as written; a seeded row is served
with the password removed and its surrounding characters kept; the
stored row untouched).
- Test names state the rule. Every placeholder is fake and every
assertion on one is its absence from what a door served.
- `connector-entry-url-credentials.test.ts` — a second block: refused at
the string for a credential name holding a tab or newline (URL and
non-URL string), a userinfo behind a scheme or `//` holding a tab or
carriage return, a scheme-relative userinfo; CONTROL accepts for an
ordinary name holding a tab, a credential value of only tab/newline
characters, a cleaned scheme with no userinfo, a scheme-relative
authority with no userinfo, an `@` in a scheme-relative path; the read
rewrites at the matching authored offsets; refused ⇔ rewritten.
- `driver-credential-refusal.test.ts` — per URL-bearing driver key:
`user:password@` refused with a tab, newline or carriage return inside
the scheme or the password; CONTROL a bare username and a password of
only that character; per query driver: the parameter refused with such a
character inside its name, CONTROL a value of only such characters; the
shared parses read pairs and userinfo with them deleted.
- `datasource-credential-redaction.test.ts` — the read removes the
password at the mapped offsets and a credential pair whose name holds
such a character, keeping the rest of the query; CONTROL strings
unchanged.
- Dogfood, both save doors: connector — 422 for a credential name
holding a tab, a scheme holding a tab, a scheme-relative userinfo;
CONTROL an ordinary name holding a tab saves, materializes and is sent
as the parser reads it, a scheme-relative URL with no userinfo saves.
Datasource postgres — 422 for a `password` name holding a tab and a
scheme holding a tab; CONTROL a bare username and an ordinary name each
with a tab save as written; a seeded row is served with its credential
parts removed at the judged positions.
## Acceptance notes
- **Basis for the two additions beyond the ruled text.** The
maintainer's ruling D is "one value-level rule at both doors"; its
stated purpose is that the check sees what the consuming parser sees.
Removing tab/newline characters before judging, and the connector using
the datasource's scheme-optional userinfo grammar, are that same rule.
They narrow beyond the ruled literal text, and the seat has put that to
the maintainer as a veto window.
- **Consumers.** The cleaning matches the WHATWG parse that
pg-connection-string, mysql2 and the mongodb driver apply to these
values; for `@libsql/core`, which applies none, it only refuses at
publish a value the client rejects at connect.
- **Cost measured.** A non-URL string under `providerConfig` holding a
credential parameter with a value is now refused. Over `examples/`,
`packages/apps/`, `skills/`, `content/docs/` and `apps/docs/` (`git
grep` for a credential name after `?` or `&` with a value): 3 hits, none
in a connector entry (a storage-service doc comment and two auth-docs
`fetch` calls); the three OpenAPI documents `packages/apps` and the
showcase reference by path: 0 hits.
- `auth` endpoints are URLs by their own declared format, so a string
with no scheme there is already refused by that format; the any-string
rule matters under `providerConfig`.
- The other package this diff touches is `packages/qa/dogfood` (private,
tests only).
## Verification
Head `49b7049edb` (`origin/main` at `9edd6e783` merged in).
- Local gate subset: `dispatch-gates --commands` derived 81 commands,
each exit captured before any pipe; `--ran`: 81 derived, 81 run, 0
NOT-MEASURED. `check:type-check-debt` exit 0 (0 raw tsc errors).
`check:api-surface`: `public API surface + factory signatures
unchanged`. `check:authorable-surface`, `check:strictness-ledger`,
`check:spec-changes`, `check:upgrade-guide`,
`check:dual-build-cjs-loads` exit 0. `check:generated`: `All 14
generated artifacts are up to date`.
- Typecheck: spec and dogfood exit 0. Four touched spec files 463/463.
Full `pnpm --filter @objectstack/spec test`: 807 files, 24224 passed, 1
todo, exit 0. Two dogfood pin files 51/51. Full `pnpm --filter
@objectstack/dogfood test`: 410 passed and 3 skipped files, 3400 passed
and 28 skipped tests, exit 0.
- Ablation, one shot per leg through `scripts/ablation-replace.mjs`,
each restored to blob == HEAD with `git diff HEAD` empty: round 1 — leg
A (no trim) 34/395 red, leg B (scheme gate on the query half) 6/395 red;
round 2 — leg C (tab/newline removal removed) 51/463 red, split into
query half only 13 and userinfo half only 38; leg D (scheme-optional
authority removed) 9/463 red.
- CI on `49b7049edb`: 55 success, 9 skipped, 0 failed.
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 790d9cf commit cfd237f
13 files changed
Lines changed: 953 additions & 82 deletions
File tree
- .changeset
- packages
- qa/dogfood/test
- spec/src
- data
- driver
- integration
- migrations/entries/semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
31 | 38 | | |
32 | 39 | | |
33 | 40 | | |
| |||
412 | 419 | | |
413 | 420 | | |
414 | 421 | | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
415 | 529 | | |
0 commit comments