Skip to content

Security: second ReDoS sweep (unauthenticated email check first; request path, docs Worker, lazy scans) #174

Closed

Description

@Madraka

Second ReDoS sweep. These were found while verifying #173 (the #168 follow-ups). All of them predate #173. The first item is reachable without authentication; fix it before 0.6.5.

Must fix (unauthenticated, CPU per request)

  1. Email check runs before the length cap. packages/api/src/routes/password-reset.ts:123 (unauthenticated POST /request) and packages/api/src/routes/invites.ts:213 run EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ (invites.ts:85) before checking email.length > 254. The domain part is ambiguous on dots, so "a@" + ".".repeat(n) + "@" backtracks in O(n²). jsonBody (packages/api/src/tokens.ts:41) has no size cap, so one request with a 100k-character email costs seconds of CPU. That kills the Worker on its CPU limit, or stalls the event loop on Node/Docker. limitPublic limits how often a client can call, not how much each call costs.
    • Fix: check the length first in both places (Studio's roles.ts:341 already does). Make EMAIL linear: a single @, then an indexOf check for the dot.
    • Fix: add a body size cap to jsonBody (for example 64 KiB; return 413 above it) for the public token routes.
    • Tests: a 100k adversarial input completes in well under 100 ms, and an oversized body gets 413.

Should fix (request path or agent input)

  1. packages/core-ts/src/redirects.ts:20 normalisePath runs /\/+$/ on every site request (astro-renderer routing.ts:167, taxonomy.ts:1327 redirectFor). Astro collapses duplicate slashes first, which mitigates it, but the result also feeds ancestorPaths, which is O(n²) in memory on runs of slashes. Use a linear trim. Do the same for joinTarget (redirects.ts:36) and packages/schedules/src/feed-check.ts:217 urlPath.

  2. apps/docs/src/worker/paths.ts:42: isPagePath = !/\.[^/]*$/.test(pathname) runs on every docs Worker GET. "/" + ".".repeat(n) + "/" is O(n²), about 150 ms at 16 KB. Use a lastIndexOf check.

  3. Lazy scans that rescan to the end when the closer is missing, all on agent, editor or upload input:

    • packages/core-ts/src/search-rows.ts:43 (fenced code at publish time)
    • packages/core-ts/src/media/svg.ts:144 (/\/\*[\s\S]*?\*\//g on uploaded SVG CSS)
    • packages/schedules/src/feed-check.ts:157 and :202
    • packages/core-ts/src/automations.ts:450

    For each one, measure with an adversarial input. Rewrite the ones that are superlinear with indexOf-based scanners.

  4. packages/agent-runtime/src/skills.ts:39 /^\s+([^:]+):\s*(.*)$/: \s+ overlaps [^:]+, so a line of only spaces is O(n²). Check whether skill markdown can come from callers. Fix it either way, it is cheap.

  5. packages/ui/src/blocks/editor/markdown-blocks.ts:69,76 /\n*$/: client-side, but a body written by an agent can reach it. Use a linear trim.

Done when

  • Every rewrite has an equivalence test against the old regex on normal inputs and a 100k-character adversarial case.
  • A short CONTRIBUTING or AGENTS.md note: avoid unanchored or end-anchored quantified runs and lazy scans on external input, and prefer indexOf scanners.
  • Patch changesets for the published packages touched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions