Skip to content

fix(ui): send task messages from plain-http origins - #13428

Open
capo-the-ai-bot wants to merge 2 commits into
paperclipai:masterfrom
TMaYaD:fix/ui-random-uuid-insecure-context
Open

capo-the-ai-bot wants to merge 2 commits into
paperclipai:masterfrom
TMaYaD:fix/ui-random-uuid-insecure-context

Conversation

@capo-the-ai-bot

@capo-the-ai-bot capo-the-ai-bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • Many operators self-host it and open the UI over plain http from another machine on a LAN, a tailnet, or a VPN
  • Browsers treat those origins as insecure contexts. They do not expose crypto.randomUUID there, only crypto.getRandomValues
  • The task page composer sends every comment with a client request ID, and it generated that ID with a direct crypto.randomUUID() call
  • On a plain-http origin the call throws, the composer's own catch swallows the error and restores the text, and no request is sent. The Send button looks dead
  • This pull request adds a shared randomUuid() helper with a getRandomValues fallback and routes every direct crypto.randomUUID() call in the UI through it
  • The benefit is that sending a message works on every origin, and the other actions with the same call (decisions, provider credentials, chat email setup) stop failing in the same silent way

Linked Issues or Issue Description

No public issue is filed for the send failure. Related references:

Bug report fields:

What happened?

On a Paperclip instance opened at http://<lan-host>:3100 or a tailnet host, typing a message on the task page and clicking Send does nothing. The text clears and comes back. No comment is posted and no toast appears. The same click works on http://localhost:3100.

Expected behavior

The message posts on every origin the UI is served from.

Steps to reproduce

  1. Run paperclipai and open the UI from a different machine over plain http, so the URL is not localhost or 127.0.0.1.
  2. Open any task. Type a message. Click Send, or press Cmd+Enter.
  3. Observe that the text is restored and POST /api/issues/:id/comments is never sent.

Local reproduction without a second machine: open a task on a dev server, run delete Crypto.prototype.randomUUID in the browser console, then send.

Paperclip version or commit

Reproduced on f2c5e54dc (master, 2026-09-14). The composer submit path has contained a direct crypto.randomUUID() call since #13038.

Deployment mode

Self-hosted (local_trusted or authenticated) with the UI opened over plain http on a non-loopback host. Verified on a local embedded-Postgres instance with the function removed from the page.

What Changed

  • ui/src/lib/random-uuid.ts (new): randomUuid() returns crypto.randomUUID() when the browser exposes it, and otherwise builds an RFC 4122 v4 UUID from crypto.getRandomValues. The output passes the server's z.string().uuid() validators. Without any CSPRNG it throws a clear error instead of producing predictable identifiers.
  • ui/src/lib/issue-execution-policy.ts: the private newId() generator moves to the shared helper.
  • ui/src/components/task-chat/TaskChatComposer.tsx: submission attempt IDs and local attachment IDs use the helper. This is the task page Send button.
  • ui/src/components/IssueChatThread.tsx and ui/src/pages/IssueDetail.tsx: the classic composer and the page-level clientRequestId fallback use the helper.
  • ui/src/components/DecisionResolver.tsx, ui/src/components/chat/ExternallyConnectedTaskBanner.tsx, ui/src/lib/provider-credential.ts, ui/src/pages/apps/chat/EmailEndpointSetup.tsx: the remaining direct crypto.randomUUID() calls use the helper. Each one fails the same way on plain http.
  • Tests: ui/src/lib/random-uuid.test.ts covers the native path, the getRandomValues fallback, distinct ids across calls, and the error raised without a CSPRNG. TaskChatComposer.test.tsx gains a send with crypto.randomUUID absent. It asserts that the posted client request ID is a valid v4 UUID and that the draft is cleared.

Verification

  • pnpm --filter @paperclipai/ui typecheck: clean.
  • pnpm exec vitest run ui/src/lib/random-uuid.test.ts ui/src/lib/issue-execution-policy.test.ts ui/src/components/task-chat/TaskChatComposer.test.tsx ui/src/components/IssueChatThread.test.tsx ui/src/components/TaskChatThread.test.tsx ui/src/pages/IssueDetail.test.tsx ui/src/components/DecisionResolver.test.tsx ui/src/components/chat/ExternallyConnectedTaskBanner.test.tsx: 8 files, 462 tests pass.
  • Manual, on a local embedded-Postgres instance: open a task, run delete Crypto.prototype.randomUUID in the console, type a message, click Send. Before the fix: no request, text restored, no toast. After the fix: POST /api/issues/:id/comments returns 201, the bubble renders, and the stored comment's clientRequestId is a valid v4 UUID.

Risks

  • Low risk. On secure origins the helper calls the same native function as before, so IDs are unchanged.
  • Fallback IDs come from getRandomValues, a CSPRNG, and carry the v4 version and variant bits. Server validators that require a UUID accept them.
  • RunnerGoalWidget and cross-tab-poll keep their own non-UUID fallbacks. Their IDs never reach a UUID validator, so they are unchanged.
  • The helper never falls back to Math.random. The execution-policy generator it replaces did. A browser without getRandomValues now gets a clear error instead of a predictable ID; no supported browser lacks it.

Model Used

  • Claude Fable 5.1 (Anthropic, model ID claude-fable-5-1) through Claude Code (desktop app), with extended thinking and tool use: shell, file edits, and an in-app browser driving a local Paperclip instance. It produced the diagnosis, the code, the tests, and this description.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have not referenced internal/instance-local Paperclip issues or links (only public GitHub #NNN / github.com/paperclipai/paperclip URLs)
  • My branch name describes the change (e.g. docs/..., fix/...) and contains no internal Paperclip ticket id or instance-derived details
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

Browsers expose crypto.randomUUID only in secure contexts (https or
localhost). On a LAN or tailnet origin the task composer's submit path
threw while generating the comment clientRequestId, the composer's own
catch swallowed the error and restored the text, and nothing was sent.
No toast appeared, so the Send button looked dead.

Move the schema-valid fallback generator that issue-execution-policy
already carried into a shared randomUuid() helper. It uses the native
function when present and otherwise builds a v4 UUID from
getRandomValues. Route the task composer, the classic composer, the
page-level fallback, and the other direct crypto.randomUUID() call
sites (decision resolution, the externally connected task banner,
provider credential setup, email endpoint setup) through it.

Refs paperclipai#3529

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Superagent found 1 security concern(s).

Comment thread ui/src/lib/random-uuid.ts
bytes[index] = Math.floor(Math.random() * 256);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Do not use Math.random as a fallback for security-sensitive identifiers

The shared UUID helper uses predictable Math.random bytes when Web Crypto is unavailable.

Require a CSPRNG for idempotency and credential identifiers; do not fall back to Math.random.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="ui/src/lib/random-uuid.ts">
<violation number="1" location="ui/src/lib/random-uuid.ts:24">
<priority>P3</priority>
<title>Do not use Math.random as a fallback for security-sensitive identifiers</title>
<evidence>When globalThis.crypto and getRandomValues are unavailable, the new shared helper fills UUID bytes with Math.random(). This helper now supplies client request IDs, idempotency keys, and provider secret-definition keys, so those identifiers can be predictable in environments without Web Crypto.</evidence>
<recommendation>Remove the Math.random fallback for identifiers used in idempotency or credential-related flows. Prefer crypto.getRandomValues and fail with a clear error when no CSPRNG exists, or use a platform-provided secure randomness source before generating the UUID.</recommendation>
</violation>
</file>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 2fb6c25. The helper now throws a clear error when crypto.getRandomValues is unavailable instead of filling bytes from Math.random. Every supported browser exposes getRandomValues, including insecure contexts, so the throw is unreachable in practice. The tests cover the fallback path, distinct ids across calls, and the error.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fix accepted. Removing the Math.random fallback and throwing when crypto.getRandomValues is unavailable correctly eliminates predictable identifier generation for security-sensitive flows. This fail-secure approach is appropriate since modern browsers universally expose getRandomValues, and the added tests validate the error path and uniqueness guarantees.

@greptile-apps

greptile-apps Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Replaces crypto.randomUUID with a fallback helper for plain-http origins.

The PR appears safe to merge based on the reviewed changes.

Summary

The PR adds a shared UUID helper that uses crypto.getRandomValues when crypto.randomUUID is unavailable on plain-HTTP origins. It routes UI request IDs and other UUID call sites through the helper and adds fallback-path tests. No changes were made since the previous review.

Reviews (3) · Last reviewed commit: "fix(ui): require a CSPRNG for generated ..."

The shared helper filled UUID bytes from Math.random when neither
crypto.randomUUID nor crypto.getRandomValues existed, a branch carried
over from the execution-policy generator. These IDs serve as
idempotency keys and secret-definition suffixes, so predictable bytes
are wrong even on that unreachable path. Throw a clear error instead;
every supported browser exposes getRandomValues, including insecure
contexts.
@lecaj

lecaj commented Sep 22, 2026

Copy link
Copy Markdown

Confirmed on 2026.916.0. On a LAN instance over plain HTTP, task comments did not send, with no error. A local build with the same fallback approach fixed it. This is probably also the main cause of #13767. The versions match, and the silent catch matches the reported symptoms.

One call site is not in this PR: ui/src/features/connections/remote-mcp/RemoteMcpConnectionSetup.tsx. The "Add header" button calls crypto.randomUUID() directly, so it throws on plain-HTTP origins too.

@sergio-magnify

Copy link
Copy Markdown

I hit this on a self-hosted board opened over plain HTTP from another machine on the LAN (http://192.168.x.x:3100). I confirmed the cause in the browser: isSecureContext is false and crypto.randomUUID is undefined, so Send did nothing. I applied the same randomUuid() approach locally and it fixes the problem.

One gap appears after a rebase. master added ui/src/features/connections/remote-mcp/RemoteMcpConnectionSetup.tsx after this branch was cut, and that file has another unguarded call in the "Add header" button:

onClick={() => change({ headers: [...s.headers, { id: crypto.randomUUID(), name: "", value: "" }] })}

On a plain-HTTP origin this throws, and the button does nothing. Please replace it with randomUuid() when you rebase. The other remaining crypto.randomUUID calls on master already have fallbacks: cross-tab-poll.ts, RunnerGoalWidget.tsx, useDocumentAnnotationMutations.ts, optimistic-issue-comments.ts and WizardToolsStep.tsx.

Refs #13858, #13767.

@trixy-the-ai-bot
trixy-the-ai-bot force-pushed the fix/ui-random-uuid-insecure-context branch from 2fb6c25 to 0d7080c Compare September 26, 2026 12:53
@commitperclip

commitperclip Bot commented Sep 26, 2026

Copy link
Copy Markdown

Hey @capo-the-ai-bot! Before this PR can be reviewed, a few things need attention:

Informational:

  • This branch carries commits by Contributor. Squash-merging drops that authorship unless the squash message carries their trailers, and nothing else will notice if it does not. Add to the squash body when merging:

    Co-Authored-By: Contributor <contributor@example.invalid>
    

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

This branch has not been deployed

No deployments
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.

3 participants