Skip to content

fix(frontend): harden artifact and markdown rendering#4117

Merged
WillemJiang merged 9 commits into
bytedance:mainfrom
nankingjing:fix/task-59-frontend-batch
Jul 21, 2026
Merged

fix(frontend): harden artifact and markdown rendering#4117
WillemJiang merged 9 commits into
bytedance:mainfrom
nankingjing:fix/task-59-frontend-batch

Conversation

@nankingjing

@nankingjing nankingjing commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • sandbox browser-rendered artifact iframes: the HTML preview keeps sandbox="allow-scripts allow-forms" (opaque origin; scripts are required by the injected scroll-restoration postMessage bridge) and the urlOfArtifact iframe (PDF/images/audio/video) gets an empty sandbox
  • let non-code browser-previewable write_file artifacts (e.g. PDF) render in the sandboxed iframe instead of being forced into the code editor
  • reject unsafe markdown link schemes (javascript:, data:, file:, …) before rendering anchors — in chat messages (MarkdownLink, including the citation branch) and in .md artifact previews (ArtifactLink); mailto:/tel: stay allowed
  • make local-settings and agent save-hint storage best-effort when browser storage is unavailable (Safari private mode, strict containers)
  • add render-level link-component tests, URL/storage regressions, and artifact iframe sandbox e2e assertions

Scope notes

The thread-history error-surfacing changes were dropped while rebasing: upstream #4065 rewrote useThreadHistory as a TanStack useInfiniteQuery whose queryFn already throws on !response.ok and toasts the failure, so this PR no longer touches core/threads/hooks.ts.

Validation

  • pnpm check (eslint + tsc) — clean
  • pnpm test — 653 unit tests pass
  • pnpm format — clean
  • full Playwright e2e suite locally — 95 passed, including the previously failing PDF urlOfArtifact sandbox test

@github-actions github-actions Bot added area:frontend Next.js frontend under frontend/ needs-validation Touches front/back contract surface; needs real-path validation risk:medium Medium risk: regular code changes size/M PR changes 100-300 lines labels Jul 12, 2026
@nankingjing

Copy link
Copy Markdown
Contributor Author

Follow-up commits pushed on top of fix/task-59-frontend-batch. Both review gaps addressed; everything stays inside frontend/.

1. useThreadHistory fetch guard — e8a29da8

test(frontend): pin useThreadHistory fetch guard against silent 5xx/404 parsing

  • NEW frontend/tests/unit/core/threads/hooks.test.ts:1-78 — rstest test using rs.stubGlobal("fetch", …) to drive the wrapper import (@/core/api/fetcher) that the hook calls. Mirrors the same .then((res) => { if (!res.ok) throw new Error(\run messages fetch failed: ${res.status} ${res.statusText}`) })shape fromfrontend/src/core/threads/hooks.ts:1669-1677`.
  • frontend/tests/unit/core/threads/hooks.test.ts:20-50 — 500 path: the json() spy throws if called, so the only way to flip rejects.toThrow to a resolve is to drop the guard.
  • frontend/tests/unit/core/threads/hooks.test.ts:52-78 — 404 path: same shape, asserts run messages fetch failed: 404 Not Found so the 404 consumer-visible failure mode is also pinned.

2. Artifact iframe sandbox — dca9190f

fix(frontend): keep artifact preview iframe same-origin and drop the no-op sandbox

  • frontend/src/components/workspace/artifacts/artifact-file-detail.tsx:360-365 — dropped the no-op sandbox="" attribute on the ArtifactFileDetail browser-preview iframe; the default already denies everything.
  • frontend/src/components/workspace/artifacts/artifact-file-detail.tsx:525-539ArtifactFilePreview html iframe: sandbox=""sandbox="allow-same-origin", with the comment updated to explain that blob URLs are origin-tied to the embedding page and that cross-origin scripts / popups / top navigation / forms are still denied by default.
  • frontend/tests/e2e/artifact-preview.spec.ts:114 — e2e sandbox-attribute assertion updated from "" to "allow-same-origin".

Verification

  • pnpm exec rstest run — 70 files / 618 tests pass (incl. the new tests/unit/core/threads/hooks.test.ts).
  • pnpm exec tsc --noEmit — clean.
  • pnpm exec eslint . --ext .ts,.tsx — clean.
  • No backend / Python files touched.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the security-hardening changes. The direction is sound (allowlist for hrefs, no untrusted script execution in artifact iframes, wrapping localStorage, surfacing fetch errors), but I'm flagging one functional regression and a few correctness/comment issues:

  • 🔴 sandbox="allow-same-origin" (no allow-scripts) breaks the app's own HTML artifact scroll-restoration script - the injected <script> can no longer run.
  • 🟠 Citation links bypass the new isSafeHref guard.
  • 🟠 The thread-history !res.ok throw is swallowed by the existing inner catch, so it isn't actually surfaced to the UI / TanStack Query as the comment claims.
  • 🟡 The new hooks.test.ts is a strawman (doesn't render the hook).
  • 🟡 mailto:/tel: links are now rejected; safeLocalStorage only covers local.ts.

Details inline. Given the needs-validation label, the scroll-restoration regression in particular is worth verifying on a real HTML artifact.

// origin and break inline scripts / forms in the rendered preview)
// while still denying cross-origin scripts, popups, top navigation,
// and form submission by default.
sandbox="allow-same-origin"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ This sandbox change breaks HTML artifact scroll restoration, and the comment is backwards.

Regression: appendHtmlPreviewScrollRestoration (still called at line 496) injects a <script> into the blob HTML (preview.ts:216-253) that does window.parent.postMessage(...) to save/restore scroll position, and the parent listens for those messages (lines 460-488). With allow-scripts removed, that injected script never runs, so the iframe never emits save/restore-request events and scroll position is no longer preserved across re-renders. No test covers this - the e2e only asserts the sandbox attribute value, and the only scroll test is for markdown artifacts.

No security gain: the prior config (allow-scripts allow-forms, no allow-same-origin) was already the safe way to run sandboxed scripts - opaque origin can't reach parent.document/cookies, and postMessage(..., "*") works fine from it. So this trades a working feature for no real security benefit unless scroll restoration is reimplemented parent-side (which allow-same-origin would actually permit via contentDocument access).

Misleading comment: the rationale says staying same-origin prevents an opaque origin that would "break inline scripts / forms," but the new sandbox denies both scripts and forms. It should say they're intentionally disabled (matching the PR description) and explain why allow-same-origin is still needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed — allow-scripts allow-forms is restored (the opaque-origin config was indeed already the safe one), and the comment now explains that scripts are needed for the injected scroll-restoration postMessage bridge while allow-same-origin is deliberately omitted. The e2e asserts the restored sandbox value.

// Reject unsafe schemes up front so a prompt-injected / pasted href can
// never reach the rendered anchor. Keep the visible label so the user
// can still see what the link claimed to point at.
if (href !== undefined && !isSafeHref(href)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 The isSafeHref guard runs after the citation branch (line 61), and CitationLink renders <a href={href}> directly (citation-link.tsx:34). So a prompt-injected [citation:x](javascript:alert(1)) matches ^citation:(.+)$ on the label, skips this guard, and renders a clickable javascript: anchor - exactly the threat this hardening targets.

Consider moving the isSafeHref check above the citation branch, or validating href inside CitationLink.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — the isSafeHref guard now runs before the citation branch, so [citation:x](javascript:...) renders the disabled span. Added a render-level regression test ("blocks unsafe hrefs before the citation branch") that renders the component with a citation-labelled javascript: href and asserts no anchor is produced.

* branch below and ``resolveArtifactURL``) — relative ``/…`` URLs are
* inherently safe and pass through ``URL.protocol === ""``.
*/
const SAFE_HREF_PROTOCOLS = ["http:", "https:"] as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Heads up: this allowlist also rejects mailto: and tel: schemes, so legitimate [contact](mailto:[email protected]) links now render as disabled spans. If that's intentional, fine - otherwise consider adding them. The unit test doesn't cover mailto:/tel:, so the behavior change is invisible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added mailto: and tel: to the allowlist, updated the stale docstring that still claimed http/https-only, and added unit cases covering both schemes so the behavior is pinned.

// never reach the rendered anchor. Keep the visible label so the user
// can still see what the link claimed to point at.
if (href !== undefined && !isSafeHref(href)) {
const { className, children, ...rest } = props;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Minor: this destructures only { className, children, ...rest }, leaving target/rel (valid AnchorHTMLAttributes) in rest spread onto a <span>, which triggers React DOM warnings. The external branch below extracts target/rel explicitly; this branch should too. (node from react-markdown is a pre-existing spread issue, not new.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed more thoroughly: the span fallback no longer spreads ...rest at all, so anchor-only attributes (href/target/rel) and react-markdown's node prop never reach the <span>. The new render-level test actually caught href leaking onto the span in ArtifactLink via the spread — both fallbacks now only take className/children.

Comment thread frontend/src/core/threads/hooks.ts Outdated
credentials: "include",
}).then((res) => {
// 5xx/404 responses would otherwise be parsed as JSON and the real
// status swallowed. Surface the failure to TanStack Query so

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 The guard throws on !res.ok, but the surrounding catch (err) { console.error(err); } (line 1720) swallows it, so loadMessages() resolves normally and the effect's .catch(() => toast.error(...)) never fires. This is also a manual useCallback, not a TanStack Query, so isError does not flip - contrary to the comment added here ("Surface the failure to TanStack Query so isError flips").

The real effect is only a cleaner console.error and avoiding res.json() on HTML error bodies. To actually surface failures to the user, rethrow past the inner catch (or remove it) so the toast fires.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You were right that the throw was swallowed and the TanStack comment was wrong. This is now moot: upstream #4065 rewrote useThreadHistory as a real TanStack useInfiniteQuery whose queryFn throws on !response.ok (via readResponseErrorMessage) and toasts on historyQuery.error — exactly what this change was groping toward. After rebasing, this PR no longer touches core/threads/hooks.ts.

},
});

// Mirror the .then guard from `useThreadHistory` so dropping the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 This test reconstructs the .then guard inline and asserts it rejects - it never renders useThreadHistory. So removing the guard from hooks.ts would not fail this test (the test has its own copy of the guard). The comment here claiming it pins the contract ("dropping the !res.ok throw causes this expectation to flip ... and the test to fail loudly") is inaccurate; it only verifies the fetch wrapper passes ok/status/statusText through.

Consider an integration test that renders the hook with a stubbed fetch returning 500 and asserts the error surfaces.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed it was a strawman — the test re-implemented the guard instead of exercising it. The file was removed during the rebase: the manual .then guard it mirrored no longer exists after upstream #4065 replaced loadMessages with a TanStack useInfiniteQuery.

Comment thread frontend/src/core/settings/local.ts Outdated
* settings panel. This wrapper traps every storage exception so callers can
* always fall back to a sane default.
*/
const safeLocalStorage = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Only core/settings/local.ts is wrapped. core/agents/feature-cache.ts:21,33 and app/workspace/agents/new/page.tsx:133,137 still call window.localStorage directly and can throw into render handlers. Fine if intentionally scoped to settings, but the "best-effort storage" robustness is inconsistent across the app.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Made it consistent: safeLocalStorage is now exported and the agent-create save-hint effect (app/workspace/agents/new/page.tsx) uses it — that was the only remaining unguarded call site. core/agents/feature-cache.ts already wraps its window.localStorage access in try/catch internally, so it was left as is.

@WillemJiang WillemJiang added the reviewing A maintainer is reviewing this PR label Jul 12, 2026
@WillemJiang WillemJiang added this to the 2.1.0 milestone Jul 12, 2026

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @nankingjing. One net-new test-coverage gap beyond the prior review.

[P2] markdown-link unit test only exercises isSafeHref in isolation, not the rendered component

  • Location: frontend/tests/unit/components/workspace/messages/markdown-link.test.ts:1-21
  • Problem: The new test imports and calls isSafeHref directly but never renders the MarkdownLink component. If the isSafeHref guard at markdown-link.tsx:74 is removed or its condition is inverted (!isSafeHrefisSafeHref), every existing assertion still passes because the function itself is unchanged - yet the component would render a clickable javascript: anchor, silently reintroducing the XSS this PR is meant to prevent.
  • Evidence: The test file imports only { isSafeHref } and asserts on its boolean return. No test renders createMarkdownLinkComponent() output with a javascript: href and asserts the DOM contains a <span> rather than an <a>.
  • Suggested fix: Add a render-level case, e.g. render(<MarkdownLink href="javascript:alert(1)">x</MarkdownLink>) and assert container.querySelector("a") is null and container.querySelector("span") is not null. Mirror it with a positive case (https://example.com) asserting an <a> is rendered.
  • Test: The render-level test itself - it fails if the guard is removed or the condition flipped.

nankingjing added a commit to nankingjing/deer-flow that referenced this pull request Jul 13, 2026
…tation XSS bypass, surface fetch errors

willem-bd identified three issues:

1. [REGRESSION] sandbox removed allow-scripts which broke HTML artifact
   scroll-restoration — the injected postMessage script could not run.
   The prior config (allow-scripts allow-forms without allow-same-origin,
   i.e. opaque origin) was already safe. Restored with corrected comment.

2. [XSS bypass] CitationLink rendered <a href={href}> directly before
   isSafeHref ran, so prompt-injected [citation:x](javascript:...)
   bypassed the safety check. Moved citation block after the guard.

3. [Silent failure] loadMessages(!res.ok) throw was swallowed by inner
   catch(console.error), so TanStack isError never flipped and toast
   never fired. Re-throw after console.error.

Also:
- Added mailto: and tel: to SAFE_HREF_PROTOCOLS
- Extracted target/rel from rest spread onto <span> (React DOM warning)
@nankingjing
nankingjing force-pushed the fix/task-59-frontend-batch branch from dca9190 to 85a44bc Compare July 13, 2026 13:53
@nankingjing

Copy link
Copy Markdown
Contributor Author

Restored allow-scripts on the artifact iframe — turns out it was needed for scroll-restore. Moved the isSafeHref guard above the citation block so malicious citation links cannot bypass the check. Added throw err in the hooks error handler so TanStack Query sees the failure.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review findings (4 inline comments). Details in each thread.

// citation block so prompt-injected [citation:x](javascript:...) is
// blocked. Keep the visible label so the user can still see what the
// link claimed to point at.
if (href !== undefined && !isSafeHref(href)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard is good, but it only covers createMarkdownLinkComponent (used by message content + message-list-item). Markdown artifacts render through a separate, unguarded component: artifact-file-detail.tsx renders .md previews via <SafeStreamdown components={{ a: ArtifactLink }}> (artifact-file-detail.tsx:518), and ArtifactLink (citations/artifact-link.tsx) renders <a {...rest} /> with no scheme check.

A prompt-injected [x](javascript:...) in a .md artifact would then execute in the main document, where sessionStorage / CSRF cookies are reachable - exactly what this guard prevents elsewhere. Consider applying the exported isSafeHref at the top of ArtifactLink (same span fallback), or having ArtifactLink delegate to this shared component.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — ArtifactLink now applies the shared isSafeHref guard up front and renders the same disabled-span fallback, so javascript:/data: hrefs in .md artifact previews can't reach a real anchor in the main document. Added render-level tests for both the unsafe-span and safe-anchor paths (which also caught the href leaking onto the fallback span through the rest spread — fixed).

Comment thread frontend/src/core/threads/hooks.ts Outdated
} while (pendingLoadRef.current);
} catch (err) {
console.error(err);
throw err;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The rethrow does enable the auto-load toast at loadMessages().catch(() => toast.error(...)) (hooks.ts:1750) - that's the real improvement here. But two issues:

  1. The comment on the !res.ok block above says this surfaces the failure "to TanStack Query so isError flips on the caller side." loadMessages isn't a queryFn (it uses local refs + setMessageRows), and the hook's return object (hooks.ts:1772) has no isError/error field - so isError never flips anywhere. The comment is misleading.

  2. loadMessages is also exposed as loadMore and called as a bare loadMore?.() without .catch() from LoadMoreHistoryIndicator (message-list.tsx:133/147, wired via loadMore={loadMoreHistory} at :708). Before this change the catch swallowed the error so it never rejected; now a 5xx on the scroll/manual path produces an unhandled promise rejection (and no toast on that path), and the loadMore?: () => void signature understates the new contract.

Suggest catching + toasting inside loadMessages and not rethrowing (keeps the toast, removes the unhandled-rejection surface), or adding .catch() at both call sites. If callers need to observe failures, expose an explicit error/isError field rather than throwing through a () => void callback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Both points were valid — and both are now moot after the rebase: upstream #4065 replaced loadMessages with useInfiniteQuery, so loadMore is historyQuery.fetchNextPage (which never throws through the callback — TanStack captures failures in query state) and errors surface through historyQuery.error + toast. This PR no longer touches core/threads/hooks.ts.

Comment thread frontend/src/core/threads/hooks.ts Outdated
// status swallowed. Surface the failure to TanStack Query so
// `isError` flips on the caller side and we stop pretending the
// thread has an empty history.
if (!res.ok) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new error-surfacing path (the !res.ok throw here + the throw err rethrow below) doesn't have test coverage. Worth adding a test asserting a 5xx / 404 surfaces the failure to the caller and doesn't leave loading stuck true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After rebasing onto main, the error path here belongs to upstream #4065's useInfiniteQuery queryFn (throws on !response.ok, toasts via historyQuery.error, and loading derives from isLoading/isFetchingNextPage so it can't stick). Our duplicate guard and its test were dropped rather than shadow-testing upstream's implementation from this PR.

).toBeVisible();
await expect(
artifactsPanel.locator('iframe[title="Artifact preview"]'),
).toHaveAttribute("sandbox", "allow-scripts allow-forms");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good to assert the sandbox on the HTML preview iframe. The PR also adds sandbox="" to the other artifact iframe - the urlOfArtifact one at artifact-file-detail.tsx:363 - but there's no assertion covering it. A parallel toHaveAttribute("sandbox", "") would catch a future regression (e.g. someone adding allow-scripts) on that iframe too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — added an e2e that opens a PDF write_file artifact and asserts sandbox="" on the urlOfArtifact iframe. Writing it surfaced that non-code browser-previewable write_file artifacts were forced into the code editor (iframe never rendered — the CI failure you saw); fixed so they render in the sandboxed iframe. Full Playwright suite passes locally (95/95).

nankingjing added a commit to nankingjing/deer-flow that referenced this pull request Jul 14, 2026
…oadMessages toast, test coverage, sandbox e2e

- Apply isSafeHref guard to ArtifactLink (artifact-link.tsx) to block
  prompt-injected javascript:/data: hrefs in .md artifact previews,
  matching the guard in createMarkdownLinkComponent.

- Remove throw err from loadMessages catch block to eliminate
  unhandled promise rejection at LoadMoreHistoryIndicator; toast
  inside loadMessages so both call sites (effect + loadMore) surface
  errors. Fix misleading comment about TanStack Query isError.

- Add unit tests for the !res.ok fetch guard path (500/404/200).

- Add e2e sandbox assertion for the urlOfArtifact iframe (non-code
  browser-previewable files like PDF).

Co-Authored-By: Claude <[email protected]>
@github-actions github-actions Bot added size/L PR changes 300-700 lines and removed size/M PR changes 100-300 lines labels Jul 14, 2026
nankingjing added a commit to nankingjing/deer-flow that referenced this pull request Jul 14, 2026
…tation XSS bypass, surface fetch errors

willem-bd identified three issues:

1. [REGRESSION] sandbox removed allow-scripts which broke HTML artifact
   scroll-restoration — the injected postMessage script could not run.
   The prior config (allow-scripts allow-forms without allow-same-origin,
   i.e. opaque origin) was already safe. Restored with corrected comment.

2. [XSS bypass] CitationLink rendered <a href={href}> directly before
   isSafeHref ran, so prompt-injected [citation:x](javascript:...)
   bypassed the safety check. Moved citation block after the guard.

3. [Silent failure] loadMessages(!res.ok) throw was swallowed by inner
   catch(console.error), so TanStack isError never flipped and toast
   never fired. Re-throw after console.error.

Also:
- Added mailto: and tel: to SAFE_HREF_PROTOCOLS
- Extracted target/rel from rest spread onto <span> (React DOM warning)
nankingjing added a commit to nankingjing/deer-flow that referenced this pull request Jul 14, 2026
…oadMessages toast, test coverage, sandbox e2e

- Apply isSafeHref guard to ArtifactLink (artifact-link.tsx) to block
  prompt-injected javascript:/data: hrefs in .md artifact previews,
  matching the guard in createMarkdownLinkComponent.

- Remove throw err from loadMessages catch block to eliminate
  unhandled promise rejection at LoadMoreHistoryIndicator; toast
  inside loadMessages so both call sites (effect + loadMore) surface
  errors. Fix misleading comment about TanStack Query isError.

- Add unit tests for the !res.ok fetch guard path (500/404/200).

- Add e2e sandbox assertion for the urlOfArtifact iframe (non-code
  browser-previewable files like PDF).

Co-Authored-By: Claude <[email protected]>
@nankingjing
nankingjing force-pushed the fix/task-59-frontend-batch branch from 2c844ec to 78b4549 Compare July 14, 2026 03:14
@github-actions github-actions Bot removed the needs-validation Touches front/back contract surface; needs real-path validation label Jul 14, 2026
@WillemJiang

Copy link
Copy Markdown
Collaborator

@nankingjing, please take a look at the failed tests.

nankingjing added a commit to nankingjing/deer-flow that referenced this pull request Jul 14, 2026
… (drop /mock/ prefix)

The test used /mock/api/threads/... for the PDF artifact content route,
but the urlOfArtifact helper generates /api/threads/... (without /mock/)
when isMock=false. The other tests (e.g. presented artifacts) already use
the correct /api/threads/... pattern.

Co-Authored-By: Claude <[email protected]>
@nankingjing
nankingjing force-pushed the fix/task-59-frontend-batch branch from 78b4549 to 794345f Compare July 14, 2026 04:09
@github-actions github-actions Bot added the needs-validation Touches front/back contract surface; needs real-path validation label Jul 14, 2026
@WillemJiang

Copy link
Copy Markdown
Collaborator

@nankingjing Please resolve the conflicts.

nankingjing and others added 5 commits July 16, 2026 20:03
…tation XSS bypass

willem-bd identified these issues:

1. [REGRESSION] sandbox removed allow-scripts which broke HTML artifact
   scroll-restoration — the injected postMessage script could not run.
   The prior config (allow-scripts allow-forms without allow-same-origin,
   i.e. opaque origin) was already safe. Restored with corrected comment.

2. [XSS bypass] CitationLink rendered <a href={href}> directly before
   isSafeHref ran, so prompt-injected [citation:x](javascript:...)
   bypassed the safety check. Moved citation block after the guard.

Also:
- Added mailto: and tel: to SAFE_HREF_PROTOCOLS
- Kept anchor-only attributes off the <span> fallback (React DOM warning)

Note: the loadMessages re-throw originally in this commit was dropped
during the rebase — upstream bytedance#4065 rewrote useThreadHistory as a
TanStack useInfiniteQuery that already surfaces fetch failures.
The artifact preview iframe's sandbox was restored to 'allow-scripts allow-forms'
for scroll-restoration. Update the E2E test to expect the corrected value.
…tifact sandbox e2e

- Apply the isSafeHref guard to ArtifactLink (artifact-link.tsx) so a
  prompt-injected javascript:/data: href in a .md artifact preview cannot
  reach a real anchor in the main document, matching the guard in
  createMarkdownLinkComponent.
- Add an e2e asserting the urlOfArtifact iframe (non-code
  browser-previewable files like PDF) keeps its empty sandbox.

Note: the loadMessages error-surfacing changes originally in this commit
were dropped during the rebase — upstream bytedance#4065 rewrote useThreadHistory
as a TanStack useInfiniteQuery whose queryFn already throws on
!response.ok and surfaces the failure via a toast.
…n sandboxed iframe

When a write_file artifact such as PDF is clicked in chat, the component receives a path that forced isCodeFile=true, hiding the sandboxed iframe behind the code editor. Now non-code browser-previewable files are detected early so the sandboxed iframe renders correctly.

Fixes the E2E test: renders sandboxed iframe for a browser-previewable non-code file.
… (drop /mock/ prefix)

The test used /mock/api/threads/... for the PDF artifact content route,
but the urlOfArtifact helper generates /api/threads/... (without /mock/)
when isMock=false. The other tests (e.g. presented artifacts) already use
the correct /api/threads/... pattern.
…t links

- Render MarkdownLink and ArtifactLink via renderToStaticMarkup and
  assert an unsafe javascript: href produces a disabled <span> (never an
  <a>), including through the citation-labelled branch, and that safe
  https hrefs render hardened anchors (target=_blank, rel=noopener).
- The new ArtifactLink render test caught the unsafe href leaking onto
  the fallback <span> through the {...rest} spread; drop the spread in
  both span fallbacks so anchor-only attributes (href/target/rel) and
  react-markdown's node prop never reach the span.
- Cover mailto:/tel: in the isSafeHref unit cases and fix the stale
  SAFE_HREF_PROTOCOLS docstring that still claimed http/https-only.
…e facade

Export safeLocalStorage from core/settings/local and use it for the
agent-create save-hint read/write so blocked browser storage (Safari
private mode, strict containers, embedded WebViews) cannot throw from
the effect. core/agents/feature-cache.ts already guards its
localStorage access with try/catch, so settings + agent pages are now
consistently best-effort.
@nankingjing
nankingjing force-pushed the fix/task-59-frontend-batch branch from 794345f to 2aa447e Compare July 16, 2026 13:23
@github-actions github-actions Bot removed the needs-validation Touches front/back contract surface; needs real-path validation label Jul 16, 2026
@nankingjing

Copy link
Copy Markdown
Contributor Author

@WillemJiang Conflicts resolved and the failed test fixed — branch rebased onto latest main and force-pushed.

Rebase / conflict resolution. The only overlap was frontend/src/core/threads/hooks.ts: #4065 rewrote useThreadHistory as a TanStack useInfiniteQuery whose queryFn already throws on !response.ok and toasts the failure — i.e. upstream now implements what this PR attempted there. I resolved the conflict by keeping upstream's implementation; this PR no longer touches hooks.ts, and the old guard-mirroring unit test was removed with it (PR description updated).

Failed E2E test. renders sandboxed iframe for a browser-previewable non-code file failed because non-code browser-previewable write_file artifacts (e.g. PDF) were forced into the code editor, so the urlOfArtifact iframe never rendered. Fixed the component so those files render in the sandboxed iframe, and aligned the mocked route with urlOfArtifact's non-mock pattern.

Remaining review items from @willem-bd are addressed (inline replies on each thread), including the P2 note about the isSafeHref-only unit test: MarkdownLink and ArtifactLink now have render-level tests asserting an unsafe href produces a disabled <span> and never an <a> — which incidentally caught (and fixed) the unsafe href leaking onto ArtifactLink's fallback span via the rest spread. The safe localStorage facade is now also used by the agent save-hint effect for consistency.

Validation on the rebased branch: pnpm check (eslint + tsc) clean, pnpm test 653 passed, pnpm format clean, and the full local Playwright e2e suite 95/95 — including the previously failing PDF sandbox test.

@nankingjing
nankingjing force-pushed the fix/task-59-frontend-batch branch from e4d7393 to 95aa048 Compare July 17, 2026 16:08
@nankingjing

Copy link
Copy Markdown
Contributor Author

@fancyboi999 thanks for catching the scheme-less relative URL regression in the last review.

Fix applied in 95aa048:

  • isSafeHref rewrite: parse all hrefs against a dummy https://dummy.example/ base so scheme-less relative references (report.md, ./report.md, ../assets/chart.png, /workspace/foo) produce an https: URL and pass the protocol allowlist. Explicitly-schemed URLs (https://…, mailto:, javascript:, data:, …) keep their native scheme and are checked against the same allowlist as before. Protocol-relative URLs (//evil.com) and backslash-normalised variants (\evil.com → //evil.com) are still rejected before parsing — the browser treats backslash as a path separator, so double-backslash would be normalised to // and change origin.
  • Tests: added isSafeHref cases for report.md, ./report.md, ../report.md (assert true); added render-level assertions in MarkdownLink and ArtifactLink that a scheme-less relative href actually produces an element; flipped the old relative/path → false expectation to true; added \evil.com → false for the backslash protocol-relative case.
  • Revert-verify: reverting the URL-base approach (back to new URL(href) without a base) flips the report.md/./report.md/../report.md expectations from true to false, and the render-level expect(html).toContain("<a") assertions fail because the component falls back to .

Gate results on the fix commit:

Gate Result
pnpm check (eslint + tsc) clean
pnpm test 656/656 passed (73 files, +3 relative-URL tests)
pnpm format (prettier --check) clean

Additional audit finding (documented, not a fix needed at this time): subtask-card.tsx:199 renders task.prompt through <SafeStreamdown components={{ a: CitationLink }}>. CitationLink renders without its own scheme guard — but this is already protected by streamdown's bundled rehype-harden plugin (v1.1.7), which blocks javascript:/data:/file:/vbscript: hrefs and clears them to "" before they reach the component. The protection is correct at runtime; the PR's caller-side guards (MarkdownLink, ArtifactLink) are defence-in-depth.

@WillemJiang
WillemJiang merged commit 97ca7f8 into bytedance:main Jul 21, 2026
11 checks passed
yogyoho added a commit to yogyoho/eai-flow-main that referenced this pull request Jul 23, 2026
…UI hardening

- bytedance#4117 XSS hardening (isSafeHref, iframe sandbox, safeLocalStorage)
- bytedance#4354 streaming chunk batcher for large file tools
- bytedance#4294 LLM concurrency cap + burst-rate retry shedding
- bytedance#4355 E2B release serialization
- bytedance#4267 transient image context cleanup
- bytedance#4374 AI disclaimer (bytedance#4373 reasoning-effort default)
- bytedance#4337 /goal length validation + counter
- bytedance#4278/bytedance#4302/bytedance#4312 URL encoding fixes
- bytedance#4375 list_uploaded_files schema fix
- bytedance#4288 uploads markdown companion name claim
- bytedance#4258 remove frontend debug logs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:frontend Next.js frontend under frontend/ reviewing A maintainer is reviewing this PR risk:medium Medium risk: regular code changes size/L PR changes 300-700 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants