Skip to content

fix(web): bound and validate the onboarding extract-content batch - #1528

Closed
SEPURI-SAI-KRISHNA wants to merge 1 commit into
supermemoryai:mainfrom
SEPURI-SAI-KRISHNA:fix/web-bound-onboarding-inputs
Closed

fix(web): bound and validate the onboarding extract-content batch#1528
SEPURI-SAI-KRISHNA wants to merge 1 commit into
supermemoryai:mainfrom
SEPURI-SAI-KRISHNA:fix/web-bound-onboarding-inputs

Conversation

@SEPURI-SAI-KRISHNA

Copy link
Copy Markdown
Contributor

Summary

/api/onboarding/extract-content forwards a caller-supplied urls array straight to
https://api.exa.ai/contents with livecrawl: "fallback". The only validation today is
that every element is a non-empty string:

if (!urls.every((url) => typeof url === "string" && url.trim())) { ... }

body: JSON.stringify({ urls, text: true, livecrawl: "fallback" })

The array is unbounded, entries are never parsed or de-duplicated, and the scheme is
never checked. Each entry is a potentially live-crawled — and billed — Exa request, so a
single call carrying thousands of URLs (or the same URL thousands of times) turns into a
proportionally large invoice.

Changes

  • Cap the batch at MAX_URLS = 20 and reject larger payloads with 400.
  • Parse each entry with new URL() and reject malformed values with 400, instead of
    passing junk through to a paid API.
  • Require http: or https:, so schemes like file: / data: never reach Exa.
  • De-duplicate on the normalized href, so a repeated URL is billed once.

Behaviour for well-formed input is unchanged, apart from the URLs being sent in
normalized form.

Note for reviewers: this route appears to be unused

While checking the call site to pick a safe MAX_URLS, I could not find any caller for
this route in the monorepo. The only /api/* route the web client fetches is /api/og
(apps/web/components/memories-grid.tsx:157).

The same appears to hold for its two siblings, /api/onboarding/research and
/api/onboarding/account-status. All three were added in #672 (Jan 2026), and the
onboarding flow has since been rebuilt more than once — #904 (remove unused old onboarding flow), #1067, #1178 — which looks like it dropped the callers and left the
routes in place.

I have only grepped this repository, so I cannot rule out a caller outside the monorepo,
which is why this PR hardens rather than removes. If these routes are in fact dead,
deleting all three would be the better fix — it removes a network-reachable surface that
spends money against Exa and xAI — and I am happy to open that PR instead.

Testing

biome check passes on the changed file. apps/web has no test runner configured (no
vitest dependency, no test script), so no tests were added.

  The urls array was forwarded to Exa's billed live-crawl endpoint with no
  length cap, no parsing, and no scheme check, so one request carrying
  thousands of URLs (or the same URL repeated) became a proportionally
  large invoice.

  Cap the batch at 20, parse each entry with new URL(), require http/https,
  and de-duplicate on the normalized href.
)
}

let parsed: URL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Variables and constants rule states: use const by default and use let only when reassignment is needed. The variable parsed is declared with let but is only ever assigned once (inside the try block) and never reassigned. It should be declared with const: const parsed: URL = new URL(url.trim()) (and the try/catch restructured accordingly, e.g. by moving the assignment inside the try block as a const).

Spotted by Graphite (based on custom rule: TypeScript style guide (Google))

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

MaheshtheDev added a commit that referenced this pull request Aug 23, 2026
…pt context

Parse and normalize urls with new URL() and drop duplicates before calling the paid Exa API, and collapse whitespace in name/email so a newline can't forge extra prompt lines.

Both improvements are adapted from #1528 and #1530.

Co-Authored-By: SEPURI-SAI-KRISHNA <206394534+SEPURI-SAI-KRISHNA@users.noreply.github.com>
@MaheshtheDev

Copy link
Copy Markdown
Member

Thanks @SEPURI-SAI-KRISHNA — closing this in favour of #1589, which bundles the extract-content bounds together with the session-verification fix for the same route. That route was reachable unauthenticated, so the two changes needed to land together.

Your URL handling was better than what we had: #1589 now parses with new URL(), checks protocol, and de-duplicates before calling Exa — adapted from this PR, with you credited as co-author on that commit. One difference: #1589 caps the batch at 10 rather than 20, and also caps each URL at 2048 characters.

#1589 is still in draft while it goes through review — happy to reopen this if that changes.

graphite-app Bot pushed a commit that referenced this pull request Aug 23, 2026
Cherry-picks #1579 and #1580 from @Sravanjangam (security audit #1578), plus improvements on top.

- `/api/og`, `/api/onboarding/extract-content` and `/api/onboarding/research` now verify the session against the auth backend; the middleware only checked that a cookie was present, so a forged cookie reached handlers that spend metered Exa/xAI quota.
- Bounds those routes: 2MB cap on fetched HTML, max 10 http(s) URLs per request, name/email length limits and a 60s timeout on the LLM call.
- De-duplicates URLs before calling Exa, and collapses whitespace in `name`/`email` so a newline can't forge extra prompt lines. Both adapted from @SEPURI-SAI-KRISHNA's #1528 and #1530.
- Deletes the unused, unauthenticated `account-status` route.

Verified locally: pre-fix `/api/og` returned 200 for a forged cookie, post-fix it returns 401. Five duplicate URLs collapse to two before reaching Exa, and a newline-laden `name` arrives as a single prompt line.

Supersedes #1528 and #1530.
@SEPURI-SAI-KRISHNA

Copy link
Copy Markdown
Contributor Author

Thanks for picking this up in #1589, and for the note on the URL handling.

One small thing: the merged commit (3b0fc9c9) doesn't carry a Co-authored-by: trailer, so the credit lives only in the commit body and PR description and GitHub doesn't pick it up as attribution.

Not worth rewriting an already-merged commit over, so nothing to action here. Just flagging it for future cherry-picks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants