Skip to content

Commit 4e077d4

Browse files
adamdotdevinselimerunkut
authored andcommitted
fix(app): guard randomUUID in insecure browser contexts (anomalyco#13237)
Co-authored-by: Selim <31136147+selimerunkut@users.noreply.github.com>
1 parent 95e9855 commit 4e077d4

File tree

5 files changed

+98
-6
lines changed

5 files changed

+98
-6
lines changed

packages/app/src/components/prompt-input/attachments.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { onCleanup, onMount } from "solid-js"
22
import { showToast } from "@opencode-ai/ui/toast"
33
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
44
import { useLanguage } from "@/context/language"
5+
import { uuid } from "@/utils/uuid"
56
import { getCursorPosition } from "./editor-dom"
67

78
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
@@ -31,7 +32,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
3132
const dataUrl = reader.result as string
3233
const attachment: ImageAttachmentPart = {
3334
type: "image",
34-
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
35+
id: uuid(),
3536
filename: file.name,
3637
mime: file.type,
3738
dataUrl,

packages/app/src/context/comments.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
44
import { useParams } from "@solidjs/router"
55
import { Persist, persisted } from "@/utils/persist"
66
import { createScopedCache } from "@/utils/scoped-cache"
7+
import { uuid } from "@/utils/uuid"
78
import type { SelectedLineRange } from "@/context/file"
89

910
export type LineComment = {
@@ -53,7 +54,7 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
5354

5455
const add = (input: Omit<LineComment, "id" | "time">) => {
5556
const next: LineComment = {
56-
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
57+
id: uuid(),
5758
time: Date.now(),
5859
...input,
5960
}

packages/app/src/utils/perf.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { uuid } from "@/utils/uuid"
2+
13
type Nav = {
24
id: string
35
dir?: string
@@ -16,8 +18,6 @@ const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`
1618

1719
const now = () => performance.now()
1820

19-
const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2)
20-
2121
const navs = new Map<string, Nav>()
2222
const pending = new Map<string, string>()
2323
const active = new Map<string, string>()
@@ -94,7 +94,7 @@ function ensure(id: string, data: Omit<Nav, "marks" | "logged" | "timer">) {
9494
export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) {
9595
if (!dev) return
9696

97-
const id = uid()
97+
const id = uuid()
9898
const start = now()
9999
const nav = ensure(id, { ...input, id, start })
100100
nav.marks["navigate:start"] = start
@@ -109,7 +109,7 @@ export function navParams(input: { dir?: string; from?: string; to: string }) {
109109
const k = key(input.dir, input.to)
110110
const pendingId = pending.get(k)
111111
if (pendingId) pending.delete(k)
112-
const id = pendingId ?? uid()
112+
const id = pendingId ?? uuid()
113113

114114
const start = now()
115115
const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { afterEach, describe, expect, test } from "bun:test"
2+
import { uuid } from "./uuid"
3+
4+
const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto")
5+
const secureDescriptor = Object.getOwnPropertyDescriptor(globalThis, "isSecureContext")
6+
const randomDescriptor = Object.getOwnPropertyDescriptor(Math, "random")
7+
8+
const setCrypto = (value: Partial<Crypto>) => {
9+
Object.defineProperty(globalThis, "crypto", {
10+
configurable: true,
11+
value: value as Crypto,
12+
})
13+
}
14+
15+
const setSecure = (value: boolean) => {
16+
Object.defineProperty(globalThis, "isSecureContext", {
17+
configurable: true,
18+
value,
19+
})
20+
}
21+
22+
const setRandom = (value: () => number) => {
23+
Object.defineProperty(Math, "random", {
24+
configurable: true,
25+
value,
26+
})
27+
}
28+
29+
afterEach(() => {
30+
if (cryptoDescriptor) {
31+
Object.defineProperty(globalThis, "crypto", cryptoDescriptor)
32+
}
33+
34+
if (secureDescriptor) {
35+
Object.defineProperty(globalThis, "isSecureContext", secureDescriptor)
36+
}
37+
38+
if (!secureDescriptor) {
39+
delete (globalThis as { isSecureContext?: boolean }).isSecureContext
40+
}
41+
42+
if (randomDescriptor) {
43+
Object.defineProperty(Math, "random", randomDescriptor)
44+
}
45+
})
46+
47+
describe("uuid", () => {
48+
test("uses randomUUID in secure contexts", () => {
49+
setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
50+
setSecure(true)
51+
expect(uuid()).toBe("00000000-0000-0000-0000-000000000000")
52+
})
53+
54+
test("falls back in insecure contexts", () => {
55+
setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
56+
setSecure(false)
57+
setRandom(() => 0.5)
58+
expect(uuid()).toBe("8")
59+
})
60+
61+
test("falls back when randomUUID throws", () => {
62+
setCrypto({
63+
randomUUID: () => {
64+
throw new DOMException("Failed", "OperationError")
65+
},
66+
})
67+
setSecure(true)
68+
setRandom(() => 0.5)
69+
expect(uuid()).toBe("8")
70+
})
71+
72+
test("falls back when randomUUID is unavailable", () => {
73+
setCrypto({})
74+
setSecure(true)
75+
setRandom(() => 0.5)
76+
expect(uuid()).toBe("8")
77+
})
78+
})

packages/app/src/utils/uuid.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const fallback = () => Math.random().toString(16).slice(2)
2+
3+
export function uuid() {
4+
const c = globalThis.crypto
5+
if (!c || typeof c.randomUUID !== "function") return fallback()
6+
if (typeof globalThis.isSecureContext === "boolean" && !globalThis.isSecureContext) return fallback()
7+
try {
8+
return c.randomUUID()
9+
} catch {
10+
return fallback()
11+
}
12+
}

0 commit comments

Comments
 (0)