fix(protocol): centralize safe HTML rendering and versioned cross-language API contracts #43

Closed
opened 2026-10-14 03:11:23 +00:00 by PlasticDigits · 2 comments
Owner

Scope and source

Priority: P1. Gap coverage: GAP-014, GAP-048, GAP-049. Derived from GAP_1791945837.md, reviewed code commit f8dbe93df953a78b17cb3304b717dcf84c4cd517. Current e476a68da09645c2308cbf47b90211fcaffcb7af adds only the report, so implementation findings remain applicable. Report evidence is local reproduction/source inspection unless explicitly proposed; no production exploit or current live-readiness claim.

Coverage and sequencing: gap backlog tracker. Workstream 08. Dependencies: 05.

Current codebase

The client sanitizer unwraps disallowed parents without sanitizing descendants; no-DOM fallback uses regex. Sections/comments/analysis render raw HTML, although server ammonia protects normal current writes. Rust/TS payloads, section rules and address/amount rules are duplicated; link/entity serialization and JS character-versus-UTF8 byte limits drift. Numeric entity parsing can throw and UI threshold is hardcoded. DTOs/errors/statuses are loose and OpenAPI is a handwritten stub; large handlers duplicate transaction/signing logic.

Why this implementation is needed

Display boundaries need protection against untrusted/legacy responses while signing must stay deterministic. Shared contracts and transactional services reduce security-sensitive drift and make capabilities/new lifecycle features reviewable.

Relevant files

Use a maintained sanitizer at one safe-render component for every HTML display; fail safely without a DOM. Keep render sanitation separate from authoritative server canonicalization and never silently rewrite signed bytes. Define versioned schemas/shared golden fixtures and generate typed API DTOs/OpenAPI from real handlers, including endpoints/errors/responses/capabilities/thresholds. Match UTF8 bytes with TextEncoder and safe consistent entity decoding. Factor domain mutations into transactional services and typed chain/state/action enums; centralize frontend signing/mutations and reusable section/address conversions while preserving chain-specific checks. Avoid refactoring the wallet port merely to reduce line counts.

Constraints and guardrails

Preserve standalone offchain advisory voting, no new smart contracts or archive-node dependency, BSC as a core electorate, raw 18-decimal integer balances and separate Terra/BSC address identities. Keep Legal fail-closed transactional UI and the proven DEX Terra/Bridge EVM connectors; no browser BSC balance RPC, seed/key prompt or embedded admin/model credential. API must retain a restricted role separate from the ledger writer. Freeze at actual opening, never at draft creation or a client-supplied height. Agents own design, implementation, review and evidence under repository guidance; do not shift approvals/tests to the human or invent PASS evidence.

Acceptance criteria

All body/section/comment/analysis displays neutralize active descendants and unsafe URL schemes. Shared canonical hashes/signing vectors agree for Unicode, links, entities and multiline limits. Metadata reflects configured proposal threshold. Generated OpenAPI and frontend contracts match actual success/error shapes, and transactional invariants from 05 remain intact.

Test plan: supported paths, failure paths and concurrency

Nested wrapper, legacy body, structured sections, comments and analysis through actual rendering. Empty/valid/malformed HTML, SVG/MathML, numeric entities inside/outside Unicode range, emoji, combining characters, links and byte-boundary limits. Browser/server canonical golden tests, no-DOM behavior and protocol-version compatibility. Contract-test every route/status/error/capability with Terra and BSC payloads; rerun lifecycle/concurrency after extraction.

Test plan: attack, hack and abuse vectors

Event handlers, javascript/data URLs, malformed nesting, SVG/MathML and mutation XSS; DOM clobbering, hostile imported/API content, entity exceptions and huge inputs; forged status/chain values, canonicalization ambiguity and changed payload versions. Current probe is not proof of stored XSS through valid server-sanitized writes; verify actual attack reachability.

Verification and completion criteria

Attach render attack corpus results, cross-language golden fixtures and generated OpenAPI contract test output. Confirm signatures of unchanged canonical messages remain valid and coverage includes every HTML entry point; server sanitizer remains authoritative.

Record commit and pinned toolchain, commands/results and sanitized artifacts. Relevant Rust changes require cargo test --workspace --lib plus isolated Postgres integration (explicit guarded LEDGER_TEST_DATABASE_URL); frontend changes require npm test and production build plus relevant browser cases. Run secret scanning for new fixtures/config. Exercise fresh/upgrade migrations when schema/grants change; report skipped/live-only checks explicitly.

<!-- voting-gap-1791945837:08 --> ## Scope and source **Priority:** P1. **Gap coverage:** GAP-014, GAP-048, GAP-049. Derived from [GAP_1791945837.md](https://git.cl8y.com/code/voting/src/branch/main/gaps/GAP_1791945837.md), reviewed code commit `f8dbe93df953a78b17cb3304b717dcf84c4cd517`. Current `e476a68da09645c2308cbf47b90211fcaffcb7af` adds only the report, so implementation findings remain applicable. Report evidence is local reproduction/source inspection unless explicitly proposed; no production exploit or current live-readiness claim. Coverage and sequencing: [gap backlog tracker](https://git.cl8y.com/code/voting/issues/35). Workstream **08**. Dependencies: [05](https://git.cl8y.com/code/voting/issues/40). ## Current codebase The client sanitizer unwraps disallowed parents without sanitizing descendants; no-DOM fallback uses regex. Sections/comments/analysis render raw HTML, although server ammonia protects normal current writes. Rust/TS payloads, section rules and address/amount rules are duplicated; link/entity serialization and JS character-versus-UTF8 byte limits drift. Numeric entity parsing can throw and UI threshold is hardcoded. DTOs/errors/statuses are loose and OpenAPI is a handwritten stub; large handlers duplicate transaction/signing logic. ## Why this implementation is needed Display boundaries need protection against untrusted/legacy responses while signing must stay deterministic. Shared contracts and transactional services reduce security-sensitive drift and make capabilities/new lifecycle features reviewable. ## Relevant files - [`frontend/src/utils/sanitizeProposalHtml.ts`](https://git.cl8y.com/code/voting/src/branch/main/frontend/src/utils/sanitizeProposalHtml.ts) - [`frontend/src/utils/proposalSections.ts`](https://git.cl8y.com/code/voting/src/branch/main/frontend/src/utils/proposalSections.ts) - [`frontend/src/utils/votingPayload.ts`](https://git.cl8y.com/code/voting/src/branch/main/frontend/src/utils/votingPayload.ts) - [`frontend/src/components/proposal/ProposalSectionFields.tsx`](https://git.cl8y.com/code/voting/src/branch/main/frontend/src/components/proposal/ProposalSectionFields.tsx) - [`frontend/src/pages/VoteDetailPage.tsx`](https://git.cl8y.com/code/voting/src/branch/main/frontend/src/pages/VoteDetailPage.tsx) - [`frontend/src/services/operatorVoting.ts`](https://git.cl8y.com/code/voting/src/branch/main/frontend/src/services/operatorVoting.ts) - [`operator-voting/src/html.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/src/html.rs) - [`operator-voting/src/proposal_sections.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/src/proposal_sections.rs) - [`operator-voting/src/payload.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/src/payload.rs) - [`operator-voting/src/api.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/src/api.rs) - [`operator-voting/src/db.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/src/db.rs) - [`operator-voting/src/error.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/src/error.rs) ## Recommended solution Use a maintained sanitizer at one safe-render component for every HTML display; fail safely without a DOM. Keep render sanitation separate from authoritative server canonicalization and never silently rewrite signed bytes. Define versioned schemas/shared golden fixtures and generate typed API DTOs/OpenAPI from real handlers, including endpoints/errors/responses/capabilities/thresholds. Match UTF8 bytes with TextEncoder and safe consistent entity decoding. Factor domain mutations into transactional services and typed chain/state/action enums; centralize frontend signing/mutations and reusable section/address conversions while preserving chain-specific checks. Avoid refactoring the wallet port merely to reduce line counts. ## Constraints and guardrails Preserve standalone offchain advisory voting, no new smart contracts or archive-node dependency, BSC as a core electorate, raw 18-decimal integer balances and separate Terra/BSC address identities. Keep Legal fail-closed transactional UI and the proven DEX Terra/Bridge EVM connectors; no browser BSC balance RPC, seed/key prompt or embedded admin/model credential. API must retain a restricted role separate from the ledger writer. Freeze at actual opening, never at draft creation or a client-supplied height. Agents own design, implementation, review and evidence under repository guidance; do not shift approvals/tests to the human or invent PASS evidence. ## Acceptance criteria All body/section/comment/analysis displays neutralize active descendants and unsafe URL schemes. Shared canonical hashes/signing vectors agree for Unicode, links, entities and multiline limits. Metadata reflects configured proposal threshold. Generated OpenAPI and frontend contracts match actual success/error shapes, and transactional invariants from 05 remain intact. ## Test plan: supported paths, failure paths and concurrency Nested wrapper, legacy body, structured sections, comments and analysis through actual rendering. Empty/valid/malformed HTML, SVG/MathML, numeric entities inside/outside Unicode range, emoji, combining characters, links and byte-boundary limits. Browser/server canonical golden tests, no-DOM behavior and protocol-version compatibility. Contract-test every route/status/error/capability with Terra and BSC payloads; rerun lifecycle/concurrency after extraction. ## Test plan: attack, hack and abuse vectors Event handlers, javascript/data URLs, malformed nesting, SVG/MathML and mutation XSS; DOM clobbering, hostile imported/API content, entity exceptions and huge inputs; forged status/chain values, canonicalization ambiguity and changed payload versions. Current probe is not proof of stored XSS through valid server-sanitized writes; verify actual attack reachability. ## Verification and completion criteria Attach render attack corpus results, cross-language golden fixtures and generated OpenAPI contract test output. Confirm signatures of unchanged canonical messages remain valid and coverage includes every HTML entry point; server sanitizer remains authoritative. Record commit and pinned toolchain, commands/results and sanitized artifacts. Relevant Rust changes require `cargo test --workspace --lib` plus isolated Postgres integration (explicit guarded LEDGER_TEST_DATABASE_URL); frontend changes require `npm test` and production build plus relevant browser cases. Run secret scanning for new fixtures/config. Exercise fresh/upgrade migrations when schema/grants change; report skipped/live-only checks explicitly.
Author
Owner

Implementation status after main merge PR #53, commit cd07f00f32f04ad02f8b760ed34588fd9645919e:

  • Safe rendering, canonical signing and generated API contracts implemented with regression tests and crosslinked invariants/agent guidance.
  • Combined Rust1.88.0, isolated PostgreSQL, frontend/build/browser, OpenAPI and secret checks passed; see evidence.
  • Live release verification is still in progress. The first Coolify attempts failed before healthy acceptance. PR #54 fixes native-RPC entrypoint wiring, frontend image mode, liveness probes and pooled schema selection, with additional process/fault/privilege tests.

Services are paused while existing storage is audited and the corrected pinned release is prepared. No production wallet transaction or hardware-wallet/Legal QA is claimed. This is an interim report; the issue remains open and the final comment will map the original criteria to actual live results and any unverified cases.

Implementation status after main merge [PR #53](https://git.cl8y.com/code/voting/pulls/53), commit `cd07f00f32f04ad02f8b760ed34588fd9645919e`: - [x] Safe rendering, canonical signing and generated API contracts implemented with regression tests and crosslinked invariants/agent guidance. - [x] Combined Rust1.88.0, isolated PostgreSQL, frontend/build/browser, OpenAPI and secret checks passed; see [evidence](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-issues-36-43.md). - [ ] Live release verification is still in progress. The first Coolify attempts failed before healthy acceptance. [PR #54](https://git.cl8y.com/code/voting/pulls/54) fixes native-RPC entrypoint wiring, frontend image mode, liveness probes and pooled schema selection, with additional process/fault/privilege tests. Services are paused while existing storage is audited and the corrected pinned release is prepared. No production wallet transaction or hardware-wallet/Legal QA is claimed. This is an interim report; the issue remains open and the final comment will map the original criteria to actual live results and any unverified cases.
Author
Owner

Implemented safe rendering, authoritative canonical signing and generated API contracts through PRs #53/#54 and subsequent followups. Accepted release: 5cd9363307dc1607de952ac3243193c1a16304c7 at 2026-10-14T14:07:21Z, after PR #59 merged with CI passing. Verification evidence, frontend documentation and invariants/agent guidance.

  • Every HTML display uses the maintained DOMPurify safe-render boundary. Actual Chromium exercised eight hostile vectors across legacy bodies, six structured sections, comments, five analysis fields and six compose previews; nine tests passed including safe formatting/HTTPS/mailto preservation. No active descendants/handlers, unsafe schemes, clobbering, script effects, attack requests, dialogs/popups, navigation or API writes occurred. Hostile imported/API content reaches the actual detail renderer; compose input remains escaped plain text.
  • No-DOM rendering fails closed. Tests cover empty/valid/malformed content, SVG/MathML/mutation-XSS, huge inputs, numeric entities outside Unicode range, UTF8 byte boundaries, emoji and combining text. Four shared server/browser canonical fixtures agree on canonical bytes and hashes for Unicode, links, entities and multiline normalization.
  • Six Terra/BSC register/amend/open fixtures agree on exact v2 JSON bytes, EIP-191 hashes and ADR-36 documents/hashes. Verification and exported receipts preserve exact signed bytes; rendering cannot silently rewrite signatures. Genuine legacy/unsupported versions fail for new mutations with the documented v2 re-signing requirement. Unchanged historical messages remain verifiable where original bytes exist; missing bytes cannot be recovered from hash-only legacy rows.
  • Metadata reports the configured proposal threshold: a distinctive 4321-token value is tested through the production factory. Live metadata exposes v2, the raw decimal threshold and both chain capabilities; frontend threshold decisions consume that contract.
  • Actual handler annotations generate OpenAPI and typed frontend contracts. Captured-response schema verification includes a successful response for every declared operation, all captured error shapes and both-chain capabilities. Invalid extraction/routing/content-type/oversized requests have stable correlated error contracts. Transactional services retain #40/#42 nonce, receipt, revision, quota, freeze, immutability and concurrency guarantees, tested with both chains and real restricted database logins.
  • Server ammonia remains authoritative; render sanitation is separate from signing canonicalization. The real second-origin Chromium test canonicalizes, signs, creates and amends through the full middleware/PostgreSQL stack, then obtains the same receipt on exact replay.
  • Final live checks passed on the accepted commit: 13/13 smoke checks, four shared canonical fixtures, exact both-chain receipt replays including the original now-expired signed payloads, and runtime configuration validation. API, ledger and frontend Coolify deployments finished and all services are healthy; final live evidence.
  • Fresh frontend assets and final generated contracts/metadata passed live checks. Three private readiness windows (240+240+600 =1080 observations) all passed, with maximum Terra lag 2/BSC lag 17; the two final public windows passed 45/45 each. Both Loki and direct Coolify logs show no ledger warnings after activation at 13:58:22Z. Final storage verification retained all 15 original registrations and both original BSC events; public storage now has 17 registrations following the unfunded probes, and recovery/inspection flags are false.

Verification: Rust 1.88.0, locked dependencies and fresh guarded PostgreSQL16/SCRAM infrastructure; command CARGO_BUILD_JOBS=2 scripts/verify-release.sh. Release13 passed 74 unit/supervisor tests, 121 integration tests (66 ledger hardening), 245 schema-validated API responses and 146 frontend tests, plus production build, both nginx checks and Gitleaks. Separate Chromium evidence: 11 UI regressions, nine hostile-rendering tests and one real second-origin protocol test, using Node24.21.0, Playwright1.62.1 and Chromium151.0.7922.34.

Release12 was a failed historical run; the expected late-supervisor-exit fixture race was corrected without weakening financial assertions, then passed four fresh-database repetitions and the full release13 rerun. Earlier public sampling recorded one unexplained 503 at 14:00:58Z (44/45); the next check returned 200. It is retained in final live evidence rather than counted as a pass. The subsequent two extended public windows were fully green (90/90), alongside all 1080 private observations; acceptance is based on those final windows and does not claim transient failures are impossible.

The hostile Chromium corpus uses isolated imported-response fixtures and simulated compose access through the Legal test hatch. It proves reachable display protection, not stored XSS through valid server-sanitized writes or real-wallet/Legal QA. Frontend production gating and the existing wallet stacks remain intact. No exhaustive route×status Cartesian matrix or funded-holder/hardware/mobile-wallet coverage is claimed. All required issue criteria are checked against the evidence above; no mandatory verification item remains open.

Final evidence is published through PR #60. All mandatory body/comment criteria are satisfied by the checklist above; closing this issue.

Implemented safe rendering, authoritative canonical signing and generated API contracts through PRs #53/#54 and subsequent followups. Accepted release: `5cd9363307dc1607de952ac3243193c1a16304c7` at **2026-10-14T14:07:21Z**, after [PR #59](https://git.cl8y.com/code/voting/pulls/59) merged with CI passing. [Verification evidence](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-issues-36-43.md), [frontend documentation](https://git.cl8y.com/code/voting/src/branch/main/docs/FRONTEND.md) and [invariants/agent guidance](https://git.cl8y.com/code/voting/src/branch/main/skills/AGENTS_PROTOCOL_SECURITY.md). - [x] Every HTML display uses the maintained DOMPurify safe-render boundary. Actual Chromium exercised eight hostile vectors across legacy bodies, six structured sections, comments, five analysis fields and six compose previews; nine tests passed including safe formatting/HTTPS/mailto preservation. No active descendants/handlers, unsafe schemes, clobbering, script effects, attack requests, dialogs/popups, navigation or API writes occurred. Hostile imported/API content reaches the actual detail renderer; compose input remains escaped plain text. - [x] No-DOM rendering fails closed. Tests cover empty/valid/malformed content, SVG/MathML/mutation-XSS, huge inputs, numeric entities outside Unicode range, UTF8 byte boundaries, emoji and combining text. Four shared server/browser canonical fixtures agree on canonical bytes and hashes for Unicode, links, entities and multiline normalization. - [x] Six Terra/BSC register/amend/open fixtures agree on exact v2 JSON bytes, EIP-191 hashes and ADR-36 documents/hashes. Verification and exported receipts preserve exact signed bytes; rendering cannot silently rewrite signatures. Genuine legacy/unsupported versions fail for new mutations with the documented v2 re-signing requirement. Unchanged historical messages remain verifiable where original bytes exist; missing bytes cannot be recovered from hash-only legacy rows. - [x] Metadata reports the configured proposal threshold: a distinctive 4321-token value is tested through the production factory. Live metadata exposes v2, the raw decimal threshold and both chain capabilities; frontend threshold decisions consume that contract. - [x] Actual handler annotations generate OpenAPI and typed frontend contracts. Captured-response schema verification includes a successful response for every declared operation, all captured error shapes and both-chain capabilities. Invalid extraction/routing/content-type/oversized requests have stable correlated error contracts. Transactional services retain #40/#42 nonce, receipt, revision, quota, freeze, immutability and concurrency guarantees, tested with both chains and real restricted database logins. - [x] Server ammonia remains authoritative; render sanitation is separate from signing canonicalization. The real second-origin Chromium test canonicalizes, signs, creates and amends through the full middleware/PostgreSQL stack, then obtains the same receipt on exact replay. - [x] Final live checks passed on the accepted commit: **13/13 smoke checks**, four shared canonical fixtures, exact both-chain receipt replays including the original now-expired signed payloads, and runtime configuration validation. API, ledger and frontend Coolify deployments finished and all services are healthy; [final live evidence](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-final-live.json). - [x] Fresh frontend assets and final generated contracts/metadata passed live checks. Three private readiness windows (**240+240+600 =1080 observations**) all passed, with maximum Terra lag 2/BSC lag 17; the two final public windows passed **45/45 each**. Both Loki and direct Coolify logs show no ledger warnings after activation at 13:58:22Z. Final storage verification retained all 15 original registrations and both original BSC events; public storage now has 17 registrations following the unfunded probes, and recovery/inspection flags are false. Verification: Rust **1.88.0**, locked dependencies and fresh guarded PostgreSQL16/SCRAM infrastructure; command `CARGO_BUILD_JOBS=2 scripts/verify-release.sh`. [Release13](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-release13.json) passed **74 unit/supervisor tests, 121 integration tests (66 ledger hardening), 245 schema-validated API responses and 146 frontend tests**, plus production build, both nginx checks and Gitleaks. Separate Chromium evidence: **11 UI regressions, nine hostile-rendering tests and one real second-origin protocol test**, using Node24.21.0, Playwright1.62.1 and Chromium151.0.7922.34. Release12 was a failed historical run; the expected late-supervisor-exit fixture race was corrected without weakening financial assertions, then passed four fresh-database repetitions and the full release13 rerun. Earlier public sampling recorded **one unexplained 503 at 14:00:58Z (44/45)**; the next check returned 200. It is retained in [final live evidence](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-final-live.json) rather than counted as a pass. The subsequent two extended public windows were fully green (**90/90**), alongside all 1080 private observations; acceptance is based on those final windows and does not claim transient failures are impossible. The hostile Chromium corpus uses isolated imported-response fixtures and simulated compose access through the Legal test hatch. It proves reachable display protection, not stored XSS through valid server-sanitized writes or real-wallet/Legal QA. Frontend production gating and the existing wallet stacks remain intact. No exhaustive route×status Cartesian matrix or funded-holder/hardware/mobile-wallet coverage is claimed. All required issue criteria are checked against the evidence above; no mandatory verification item remains open. Final evidence is published through [PR #60](https://git.cl8y.com/code/voting/pulls/60). All mandatory body/comment criteria are satisfied by the checklist above; closing this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/voting#43
No description provided.