Guard every URL decode behind decodedOrNull - #174
Merged
Merged
Conversation
TheGreatAxios
force-pushed
the
cl-6434-decode-hardening
branch
from
August 20, 2026 23:31
b0c23ff to
150629e
Compare
CL-6434: sweeps apps/web and packages/ for every unguarded decodeURIComponent call reachable from a URL and adds the red half of each fix first: - @corbits/url-path's decodedOrNull (the shared helper every guarded call site below routes through). - workbench-path.ts's id/section/entity parsers, including an unrecognized (never mind malformed) section id. - insights-path.ts's route parser: a malformed workbench/run id now reads as the landing default, not a detail mode with no entity — and a render test proves InsightsPage actually renders that landing dashboard for both malformed paths, not just that the parse result looks right. - kind-filter.ts's Files deep-link id parser. - chat/routes.ts's participant-removal route: a malformed address 400s instead of 500ing. - apps/hub's createStaticHandler: the SPA fallback sees every unauthenticated, attacker-controlled request path ahead of the platform's own routes, and decodeURIComponent's throw there was a 500 on any non-/api request with a malformed escape (e.g. `GET /%zz`) — the highest-value call site in this sweep. - isWorkbenchSettingsSectionId, validating a decoded section id against WorkbenchSettingsSectionId's own values instead of the unchecked `as` cast that was there before. Claude-Session: https://claude.ai/code/session_01Shhie5zM8L54bLHq5gFQti
Routes every URL-reachable decodeURIComponent call in apps/web and
packages/ through one shared, guarded helper instead of leaving each
call site to throw on a malformed percent-escape:
- New leaf package @corbits/url-path (no deps, safe in the browser
bundle and hub code alike) holding decodedOrNull —
packages/artifact-ui, packages/chat, and apps/hub can't depend on
apps/web, so the helper can't stay local to path-ids.ts.
- apps/web/src/path-ids.ts imports it instead of keeping its own copy.
- apps/web/src/workbench-path.ts: workbenchIdFromPath,
workbenchSettingsSectionFromPath (now also validated against
WorkbenchSettingsSectionId via the new isWorkbenchSettingsSectionId,
replacing the unchecked `as` cast — packages/chat-ui exports both
the guard and its backing WORKBENCH_SETTINGS_SECTION_IDS list),
workbenchSettingsEntityIdFromPath.
- apps/web/src/insights-path.ts (moved out of the 1700-line
insights-page.tsx, which now just imports it): parseInsightsPath's
workbenchId/runId. A malformed escape now falls all the way back to
{mode: "landing"} — returning {mode: "workbench", workbenchId:
null} would render a bench-scoped dashboard mislabeled "All
workbenches" with permanently-empty panels instead of the plain
landing view every other unresolvable path already gets.
- packages/artifact-ui/src/kind-filter.ts: libraryArtifactIdFromPath.
- packages/chat/src/routes.ts: DELETE .../participants/:address now
400s on a malformed address instead of throwing mid-request.
- apps/hub/src/index.ts's createStaticHandler (exported for the new
test): the SPA static-file fallback ahead of every non-/api route,
reachable unauthenticated — GET /%zz threw a 500 before this;
now falls through to the platform's own 404 like any other
unresolvable path.
packages/connections/src/oauth-routes.ts's decode was already
try/caught and is left as-is. Not in scope: apps/hub's
dbConfigFromUrl decodes a database URL's own user/password at boot —
operator-supplied config, never a client-reachable URL, so it's
noted here rather than guarded (its shape is mirrored by six test
doubles across apps/hub, packages/insights, packages/inbox, and
packages/approvals, none of them production code either).
Claude-Session: https://claude.ai/code/session_01Shhie5zM8L54bLHq5gFQti
TheGreatAxios
force-pushed
the
cl-6434-decode-hardening
branch
from
August 21, 2026 00:48
150629e to
817ea5b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
decodedOrNullinapps/web/src/path-ids.tsbut flagged remaining unguardeddecodeURIComponentcall sites inworkbench-path.tsandinsights-page.tsx. This sweeps all ofapps/webandpackages/(and, per review,apps/hub) for the rest.@corbits/url-path(no deps) holds the one guardeddecodedOrNullhelper —apps/web,packages/artifact-ui,packages/chat, andapps/huball needed it, and none of the latter three can depend onapps/web, so it couldn't stay local topath-ids.ts.apps/web/src/workbench-path.ts(workbenchIdFromPath,workbenchSettingsSectionFromPath,workbenchSettingsEntityIdFromPath).apps/web/src/insights-path.ts(moved out of the 1700-lineinsights-page.tsx, which now just imports it) —parseInsightsPath'sworkbenchId/runId. A malformed escape now falls all the way back to{mode: "landing"}; returning{mode: "workbench", workbenchId: null}would have rendered a bench-scoped dashboard mislabeled "All workbenches" with permanently-empty panels instead of the plain landing view. A render test mountsInsightsPageat both malformed paths and asserts the landing dashboard actually renders.packages/artifact-ui/src/kind-filter.ts(libraryArtifactIdFromPath).packages/chat/src/routes.ts(DELETE .../participants/:address, now 400s on a malformed address instead of throwing).apps/hub/src/index.ts'screateStaticHandler— the highest-value fix in the sweep: this SPA static-file fallback sits ahead of every non-/apiroute and sees any unauthenticated, attacker-controlled path.GET /%zzthrew aURIError→ 500 on any non-/apirequest before this; now it falls through to the platform's own 404, same as any other unresolvable path. Exported for a new test.workbenchSettingsSectionFromPathalso now validates the decoded section id againstWorkbenchSettingsSectionId's own values via a newisWorkbenchSettingsSectionIdguard (packages/chat-ui), replacing an uncheckedascast on untrusted input (AGENTS.md: neveras Tuntrusted input).packages/connections/src/oauth-routes.ts's decode was already try/caught.apps/hub/src/index.ts'sdbConfigFromUrldecodes a database URL's own user/password at boot — operator-supplied config, never a client-reachable URL. Its shape is mirrored by six test doubles acrossapps/hub,packages/insights,packages/inbox, andpackages/approvals(none of them production code either); noted here rather than guarded.Review fixes applied
manager-tools-scenario.test.tsfake-hub hunk (a 404 handler forPOST /api/workflow-chat/participants/messages, added before the position where main's own9299405blater added itspostedCards-recording handler for the same path) and an unrelatedconnections-toolsversion-pin repin (already landed on main as1a19da8b). Rebased onto currentorigin/mainand removed both hunks — the branch now carries only decode-hardening work. Verified:git diff origin/main HEAD -- workflows/assistant/test/manager-tools-scenario.test.tsand-- workflows/assistant/src/index.tsare both empty, andgit merge-base HEAD origin/mainequalsorigin/main's own tip, so this branch is a clean fast-forward — no merge, textual or semantic, is possible.manager-tools-scenario.test.ts'spostedCardsassertions run unmodified and pass.apps/hub/src/index.ts'screateStaticHandler(item 1 above) with a newapps/hub/test/static-handler.test.ts.{mode: "landing"}on a failed decode, and added a real DOM render test (insights-page-render.test.tsx) asserting the landing dashboard renders, not just that the parse result looks right.workbench-path.ts'sas-cast on decoded section input with validation against the knownWorkbenchSettingsSectionIdunion.parseInsightsPathout ofinsights-page.tsxinto its own module (apps/web/src/insights-path.ts), since it was exported from the page purely for test reach.Test plan
@corbits/url-path'sdecodedOrNull.apps/hub/test/static-handler.test.ts:GET /%zz404s instead of 500ing; normal SPA routes and/apipaths still behave.insights-path.test.ts: malformed escape on/insights/workbench/:idand/insights/runs/:idparses to{mode: "landing"}.insights-page-render.test.tsx:InsightsPagemounted at both malformed paths actually renders the landing dashboard (DOM assertion, not just the parse result).workbench-path.test.ts: malformed escape and unrecognized section id both read as no selection/no section.kind-filter.test.ts: malformed escape on/files/a/:idreads as no selection.packages/chat/test/routes.test.ts: malformed address on the participant-removal route 400s instead of throwing.chat-ui'smodel.test.ts:isWorkbenchSettingsSectionIdcovers every known id plus a non-member string.bun run checkgreen; trial merge againstorigin/mainconfirmed clean and semantically correct (fast-forward; main'spostedCardsassertions untouched and passing).https://linear.app/abklabs/issue/CL-6434
https://claude.ai/code/session_01Shhie5zM8L54bLHq5gFQti