Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

security(components): the ui 'html' renderer injects schema.html unsanitised — the spec declares no html component; retire the type #13449

Closed

Description

@os-zhuang

维护者速读

packages/components/src/renderers/basic/html.tsx 把元数据里的 schema.html 原样 dangerouslySetInnerHTML,没有任何消毒;该类型已注册、objectui 自己的 zod 面接受、console 构建产物的 sdui.manifest.json 里也有。spec 没有 html 组件(spec 的 kind: 'html' 页面是受限 JSX,由 sdui-parser 编译,且 sdui-parser 明确禁止 dangerouslySetInnerHTML)。于是任何能写元数据的人(北极星里作者是 AI)都能把原始 HTML 放进每个浏览者的页面——存储型 XSS,信任边界是「元数据作者」。按契约优先,建议退役而不是加消毒。

Measured (2026-10-24, HEAD 7981384)

  • Sink: ComponentRegistry.register('html', …) in packages/components/src/renderers/basic/html.tsx renders a div whose inner HTML is schema.html; the registration's own inputs row describes it as "Raw HTML content". HtmlSchema lives in packages/types/src/data-display.ts.
  • Reachable: imported by packages/components/src/renderers/basic/index.ts (the line import './html';); admitted by packages/types/src/zod/data-display.zod.ts (type: z.literal('html')); present in the built console's apps/console/dist/sdui.manifest.json.
  • No sanitiser on the path: DOMPurify is imported by 0 source files (the root dompurify override only serves a transitive dependency); PR objectui#13431 added rehype-sanitize to fields' html / richtext values and did not touch this renderer.
  • Spec side: packages/spec/src/ui/component.zod.ts mentions html only in prose; the page-level kind: 'html' (page.zod.ts, COMPILED_PAGE_KINDS) is constrained JSX compiled by @objectstack/sdui-parser, whose FORBIDDEN_ATTRS in packages/sdui-parser/src/parse.ts includes dangerouslySetInnerHTML.
  • Session token lives in localStorage (packages/auth/src/createAuthClient.ts, TOKEN_STORAGE_KEY), which makes a script injection a session-theft primitive.
  • Issue dump search for dangerouslySetInnerHTML|XSS|sanitiz over 2,012 cards: only objectui#13431 (fields). Other dangerouslySetInnerHTML sites were read and are not metadata-driven raw strings (print stylesheet, scoped CSS, shiki output, mermaid SVG, an escaped hand-rolled markdown preview).

Recommendation

  • Retire the html component type: remove the registration, HtmlSchema, its zod row and docs; rich HTML belongs to the already-sanitised fields path or to a kind: 'html' page. One ! changeset for components and types.
  • If a production consumer of type: 'html' turns up (未实测: external npm consumers), the fallback is sanitising through the same rehype-sanitize schema fields use — but that keeps an objectui-only component the spec does not know.

Effort S. Priority input: NORTH-STAR says security and data integrity never wait for the path.

Refs: objectui#13431.

Filed by the read-only whole-repo review session https://claude.ai/code/session_01BDtwYUWhLGyWLzYQjZ95Cf on 2026-10-24; the maintainer approved filing (「同意,创建任务」). Full evidence files sit in that session's scratchpad (objectui-review-report.md, findings-*.md). ⛔ Not a claim; for triage to grade and route.


Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Oct 24, 2026
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    and removed on Oct 24, 2026
  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    Contributor

    Triage: first grade — bug · security · priority:p1 · domain:ui · area:records · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-24T14:54Z

    ⛔ Not a claim, ⛔ not a dispatch. finding comes off.

    • p1: a stored-XSS sink on a published renderer. The trust boundary is "can write metadata", and the session token sits in localStorage. Security does not wait for the path.
    • Route: retire, as recommended. The spec declares no html component, so this is an objectui-only type the contract does not know. It gets one ! changeset for components and types. Under the 18.x standing order a removal is direct, with no retirement stage. Sanitising would keep a component the spec does not declare, so it is the fallback only if a real consumer turns up.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1
    Session: session_01432GBvPzfZ6CsLanp9xzkZ
    Account: sunhaolin
    Branch: claude/issue-13449-retire-html-renderer
    Worktree: objectui-issue-13449
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/components/src/renderers/basic/**, packages/types/src/data-display.ts, packages/types/src/zod/data-display.zod.ts, the docs and tests that name the html component type, .changeset/13449-retire-html-renderer.md (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate), effort: xhigh
    Clause-②: no (narrowing)
    Metadata-protocol: narrowed
    Responsibility: objectui's own code (@object-ui/components html renderer, @object-ui/types HtmlSchema + zod row) | the sanitised fields html/richtext path (objectui#13431) and the spec's compiled kind:'html' page cover rich HTML | any metadata author reaches it; no in-repo producer of type:'html' measured, external npm consumers not measured
    Thread-read: 6385669346
    Premise: holds
    Serial constraints cleared: none (no open code PR in objectui at 2026-10-24T16:49Z; no other pm:dispatched card in the lane)

    Clause-②: no — the card removes an objectui-only component type the spec does not declare: it narrows @object-ui/types' accept set (BREAKING, ! changeset) and widens nothing. It owes one contract-review-tier review before the queue.
    Premise reading at objectui origin/main a3c0da9: ComponentRegistry.register('html' at packages/components/src/renderers/basic/html.tsx:14; import './html'; at basic/index.ts:15; z.literal('html') at packages/types/src/zod/data-display.zod.ts:1652; export interface HtmlSchema at packages/types/src/data-display.ts:3697.

    Amended 2026-10-25 by the claiming seat: Clause-②: no → Clause-②: no (narrowing). The card narrows a published accept set, and a bare no declares no direction. Finding source: contract review on objectui#13475, comment 6393057967.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
    "issue": 13449,
    "status": "done",
    "branch": "claude/issue-13449-retire-html-renderer",
    "pr": "#13475",
    "session": "session_01432GBvPzfZ6CsLanp9xzkZ",
    "premise_still_valid": true,
    "summary": "Retired the raw-HTML html node type per the triage route: deleted the ui:html/bare html registration and its import, dropped html from PUBLIC_BLOCKS (so getPublicConfigs and the published sdui.manifest.json no longer carry it), removed HtmlSchema from the types entry, DataDisplaySchema and SchemaRegistry, and turned its zod arm into an unexported retiredNodeType refusal (the objectui#8802 / #12954 shape) so objectui validate refuses type: \"html\" by name with the remedy. Docs page, catalog example and the types example entry removed; known-schema-types and the catalog index regenerated by their generators; one breaking changeset marked minor with FROM/TO and the one-line fix. Reach measured at runtime before the fix: both a JSON node and a kind: 'html' page (an html tag carrying an html attribute compiled with ZERO diagnostics) injected an img element with an onerror attribute; after the fix the JSON node draws OBJUI-001 and the page compile refuses the tag (forbidden-tag, unknown-component). H2: content/docs/fields/rich-text.mdx :43 and :150 are the FIELD html type (HtmlFieldMetadata) and stay; check:doc-types had passed :43 only via the retired bare key, so it now carries a field-vocabulary exemption. H4 holds: no in-repo product producer (git grep of quoted html over every tracked file, read hit by hit). File surface went beyond the claim's list to every list naming html as a component type, as the dispatch's suggested route asked (listed in files_changed and the PR body); no governed surface (check-governed-queue-guard --test: NOT GOVERNED, 31 paths).",
    "tests": "Union at 3c6faea under the verify lock (VERDICT command-exit 0, held 52m11s shared box), exits captured to file: vitest packages/types+core+sdui-parser+cli+examples/schema-catalog Test Files 697 passed (697) Tests 17792 passed | 27 skipped; packages/components+fields Test Files 705 passed | 2 skipped (707) Tests 8517 passed | 31 skipped; apps/console Test Files 243 passed (243) Tests 2460 passed; 48 reader files outside those packages (every test naming PUBLIC_BLOCKS / getPublicConfigs / KNOWN_SCHEMA_TYPES / FIELD_TYPES_SKIP_FALLBACK / the examples file / the catalog / react-pages.md / ui:html / check-doc-component-types, plus unit-registry-absence-collision) Test Files 48 passed (48) Tests 1543 passed | 16 skipped. type-check core/components/fields/cli exit 0, types exit 2 on a vacuous compile pin in the new test ('kbd' is no SchemaRegistry key; tsc -p tsconfig.test.json named the new file, which proves it is in the program) fixed in bfd3722. Final head bfd3722 (differs from 3c6faea by that one test file): type-check types/core/components/fields/cli all exit 0; html-node-retired-13449 (types) Tests 6 passed (6). Ablation (one-shot, fix committed at bfd3722, trap-guarded): nine impl files put back from a3c0da9 incl. html.tsx, landing proved by on-disk counts (register('html' 1, import './html'; 1, PUBLIC_BLOCKS 'html', row 1, HtmlSchema arm 1, RetiredHtmlNodeSchema 0); tests resolve @object-ui/* via root vitest resolve.alias to src, so no dist in the path; five pin files Test Files 5 failed (5) Tests 11 failed | 65 passed (76) = every new assertion red, every control green; restore from HEAD: git diff HEAD 0 bytes, porcelain 0, html.tsx absent, every restored path's hash equals its HEAD blob.",
    "gates": "Local subset at bfd3722, each exit captured to file before reading: check:doc-types 0 (after the field-type exemption; it was 1 naming rich-text.mdx:43 before), check:registry-bare-names 0 ('no new bare-name collision'), check:handler-key-reads 0, check:doc-example-ids 0, check:docs-route-closure 0, check:prompt-keys 0, check:entry-guard 0, check:action-forward-parity 0, check:spec-symbols 0, check:new-line-citations 0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)'), check:control-bytes 0, check:doc-fences 0, check:unreferenced-sources 0, check:side-effects-array 0, check:doc-example-readers 0, check:icon-record-names 0, check:changeset-claims 0, check:pending-changeset-literals 0, check:test-path-roots 0, check:doc-json 0, check-changeset-presence 0 ('22 source file(s) of 7 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-no-major 0, regenerate-catalog-index.py --check 0, regenerate-known-schema-types.mjs --check 0 ('matches the registry derivation'), check-governed-queue-guard --test 0 (NOT GOVERNED). NOT MEASURED locally: check:sdui-registration-pins (exit 2, needs a console build), check:readme-exports (population collapsed, packages unbuilt), check:doc-snippets and check:doc-examples (exit 2 PRECONDITION NOT MET, need the wider build), apps/console and packages/app-shell type-check (need the whole closure), pnpm lint (CI-owned). objectui has no dispatch-gates derivation, so no --ran reconciliation exists; the subset was chosen by grepping scripts for the touched paths and symbols. CI check-runs on bfd3722 at report time: Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, Test (dist pins), label all success; Test (coverage) skipped; Type Check, Lint, Repo Gates, Build & E2E, Build Docs, Test (shard 1..16/16), Bundle Analysis, Live E2E (informational) in_progress.",
    "dogfood_battery": "n/a — objectui has no dogfood battery (the battery is objectstack's @objectstack/dogfood; this PR lands in objectui). The title carries ! and the changeset is breaking, so the repo's full farm in CI (Test shard 1..16/16, Build & E2E) is the whole-tree reading.",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "2 — (1) pr_create through the fleet relay: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos//issues/13475/assignees, read back 11837 bytes sent = stored, assignee sunhaolin; (2) this os-dev-report comment, POST /repos//issues/13449/comments via post-stamped. git push x6 (empty branch probe + 5 commits) is not a REST write.",
    "line_budget": "n/a — no skills/** or ratcheted ledger touched",
    "files_changed": [
    ".changeset/13449-retire-html-renderer.md",
    "apps/console/src/tests/html-node-retired-13449.test.tsx",
    "apps/console/src/tests/html-tier-manifest.test.ts",
    "apps/console/src/tests/public-contract.test.ts",
    "content/docs/components/basic/html.mdx (deleted)",
    "content/docs/components/basic/meta.json",
    "content/docs/components/index.md",
    "content/docs/guide/react-pages.md",
    "examples/schema-catalog/src/index.ts (regenerated)",
    "examples/schema-catalog/src/schemas/components-basic-html/basic-html.json (deleted)",
    "packages/app-shell/src/tests/widget-dom-leak-sweep.test.tsx",
    "packages/cli/src/tests/registered-types-validate-ratchet-10859.test.ts",
    "packages/cli/src/utils/known-schema-types.ts (regenerated)",
    "packages/components/src/tests/basic-renderers.test.tsx",
    "packages/components/src/renderers/basic/html.tsx (deleted)",
    "packages/components/src/renderers/basic/index.ts",
    "packages/core/src/registry/public-blocks.ts",
    "packages/fields/src/tests/registry-collision.test.ts",
    "packages/fields/src/index.tsx",
    "packages/types/examples/data-display-examples.json",
    "packages/types/src/tests/arm-named-export-8784.test.ts",
    "packages/types/src/tests/content-channel-family-d-9256.test.ts",
    "packages/types/src/tests/html-node-retired-13449.test.ts",
    "packages/types/src/tests/zod-mirror-parity.test.ts",
    "packages/types/src/data-display.ts",
    "packages/types/src/index.ts",
    "packages/types/src/registry.ts",
    "packages/types/src/zod/README.md",
    "packages/types/src/zod/data-display.zod.ts",
    "packages/types/src/zod/index.zod.ts",
    "scripts/check-doc-component-types.mjs"
    ],
    "deviations": [
    "Declared narrowing: the full vitest union ran at 3c6faea, not at the final head bfd3722; bfd3722 changes only packages/types/src/tests/html-node-retired-13449.test.ts (a compile-time alias anchored on 'statistic' instead of 'kbd'), so at bfd3722 I re-ran the five type-checks, that file's vitest and the whole local gate subset. CI runs the farm on bfd3722.",
    "File surface beyond the claim's list (claim: renderers/basic/**, the two types files, docs and tests naming the component, the changeset): packages/core/src/registry/public-blocks.ts, packages/types/src/{registry.ts,index.ts,zod/index.zod.ts,zod/README.md}, packages/types/examples/data-display-examples.json, packages/cli/src/utils/known-schema-types.ts (generated), packages/fields/src/index.tsx (regroup and comment, no behaviour change), the schema-catalog example and generated index, scripts/check-doc-component-types.mjs (one exemption; its suite ran green in the 48-file set), content/docs/guide/react-pages.md, and count/list pins in apps/console, packages/app-shell, packages/cli, packages/fields. Each is a list naming html as a component type, which the dispatch's suggested route asked to search; declared in the PR body.",
    "Harness attribution reminder asked for a model-named Co-Authored-By trailer and a 'Generated with' PR footer; commits carry the model-free pair objectui AGENTS.md requires (Claude-Session + Co-authored-by: Claude) and the PR body the session-URL footer from os-dev.md, per the precedence both files state.",
    "My first lock call (pnpm run setup) was queued without OS_VERIFY_LOCK_SLOT; its wrapper was stopped, the queued child kept its place and ran (VERDICT command-exit 0). Every later call set OS_VERIFY_LOCK_SLOT=issue-13449.",
    "A temporary probe test (apps/console/src/tests/zz-probe-13449.test.tsx) was written in the worktree to measure reach before and after, never committed, deleted before the first commit; its findings are carried by the committed console pin.",
    "Worktree removed after the PR opened (node_modules first, then git worktree remove, no --force)."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: objectstack's next .objectui-sha bump · objectstack's tracked repo-root sdui.manifest.json still names html (Raw HTML content); it is generated from objectui's pinned build by scripts/gen-sdui-manifest-node.mjs and check-sdui-manifest's staleness check forces the regenerate on the bump, which drops the entry; until then objectstack's server-side validateJsxPages admits the tag while the console's own compile refuses it · noted, not filed",
    "carrier: 承接者:无 · objectstack ADR-0080 names renderers/basic/html.tsx the 'today unsafe' escape hatch the constrained JSX tier replaces, and docs/audits/2026-08-zod-to-json-schema-fidelity.md lists HtmlSchema; both are dated records, left as written · Acceptance notes only"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 13449,
      "status": "done",
      "branch": "claude/issue-13449-retire-html-renderer",
      "pr": "https://github.com/objectstack-ai/objectui/pull/13475",
      "session": "session_01432GBvPzfZ6CsLanp9xzkZ",
      "premise_still_valid": true,
      "summary": "Patch round: in .changeset/13449-retire-html-renderer.md the line-initial 'Clause-②: no' now reads 'no (narrowing)'. One line changed, no other file. Pushed as a fast-forward (no force, no rebase) from bfd37223d to 0841978a5.",
      "tests": "No code touched; no tests run. New head 0841978a548e74c745e103544717496f1ddb7273. objectstack's clause2-line.mjs has no CLI (--help prints nothing), so it was called as a module: readClause2Line on the changeset returned {kind:declared,value:no,arm:narrowing,line:'Clause-②: no (narrowing)'}; readMetadataProtocolLine returned narrowed.",
      "gates": "Head 0841978a5. Only the clause2-line reader was run locally (output above). No other gate was run locally and CI results are not cited here; CI convergence is left to PM.",
      "dogfood_battery": "n/a — not run this round; the one-line changeset edit does not touch code",
      "mcp_calls": "0",
      "api_writes": "1 — this os-dev-report card comment via post-stamped.mjs (git push is not a REST write)",
      "open_questions": [],
      "out_of_scope_findings": []
    }
  7. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    Contributor

    ACCEPT → PR #13475 comment 6393203167 · contract review PASS → PR #13475 comment 6393106376 (supersedes FAIL 6393057967) · head 0841978 · ready + auto-merge armed 2026-10-25T04:00Z


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    Contributor

    LANDED — PR #13475

    domain:ui seat 1 · session_01432GBvPzfZ6CsLanp9xzkZ · 2026-10-25T04:16Z

    probe reading
    PR #13475 merged: true, merge commit 610d9efd5, through the merge queue
    origin/main tip 610d9efd5 feat(components,types)!: retire the raw-HTML html node type (objectui#13449) (#13475)
    content on origin/main packages/components/src/renderers/basic/html.tsx absent; register('html' in packages/components/src: 0 hits; control RetiredHtmlNodeSchema in packages/types/src/zod/data-display.zod.ts: 2 hits (the refusal arm is there, so the zero is not an empty read)

    State: the card closed completed through Fixes #13449, and pm:dispatched was removed in this act (security, priority:p1, domain:ui, area:records stay).

    Still open after this landing (named, not owned by this lane): objectstack's shipped console renders the html sink until objectstack bumps .objectui-sha past 610d9efd5. That bump regenerates the tracked sdui.manifest.json, which still names html; check-sdui-manifest forces the regenerate. The bump is objectstack's own card per the lane rule, not this card. Publishing @object-ui/* is the changesets release PR, a human action.


    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

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p1security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions