Просмотр исходного кода

fix(app): guard randomUUID in insecure browser contexts (#13237)

Co-authored-by: Selim <[email protected]>
Adam 1 неделя назад
Родитель
Сommit
81ca2df6ad

+ 2 - 1
packages/app/src/components/prompt-input/attachments.ts

@@ -2,6 +2,7 @@ import { onCleanup, onMount } from "solid-js"
 import { showToast } from "@opencode-ai/ui/toast"
 import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
 import { useLanguage } from "@/context/language"
+import { uuid } from "@/utils/uuid"
 import { getCursorPosition } from "./editor-dom"
 
 export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
@@ -31,7 +32,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
       const dataUrl = reader.result as string
       const attachment: ImageAttachmentPart = {
         type: "image",
-        id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
+        id: uuid(),
         filename: file.name,
         mime: file.type,
         dataUrl,

+ 2 - 1
packages/app/src/context/comments.tsx

@@ -4,6 +4,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useParams } from "@solidjs/router"
 import { Persist, persisted } from "@/utils/persist"
 import { createScopedCache } from "@/utils/scoped-cache"
+import { uuid } from "@/utils/uuid"
 import type { SelectedLineRange } from "@/context/file"
 
 export type LineComment = {
@@ -53,7 +54,7 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
 
   const add = (input: Omit<LineComment, "id" | "time">) => {
     const next: LineComment = {
-      id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
+      id: uuid(),
       time: Date.now(),
       ...input,
     }

+ 4 - 4
packages/app/src/utils/perf.ts

@@ -1,3 +1,5 @@
+import { uuid } from "@/utils/uuid"
+
 type Nav = {
   id: string
   dir?: string
@@ -16,8 +18,6 @@ const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`
 
 const now = () => performance.now()
 
-const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2)
-
 const navs = new Map<string, Nav>()
 const pending = new Map<string, string>()
 const active = new Map<string, string>()
@@ -94,7 +94,7 @@ function ensure(id: string, data: Omit<Nav, "marks" | "logged" | "timer">) {
 export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) {
   if (!dev) return
 
-  const id = uid()
+  const id = uuid()
   const start = now()
   const nav = ensure(id, { ...input, id, start })
   nav.marks["navigate:start"] = start
@@ -109,7 +109,7 @@ export function navParams(input: { dir?: string; from?: string; to: string }) {
   const k = key(input.dir, input.to)
   const pendingId = pending.get(k)
   if (pendingId) pending.delete(k)
-  const id = pendingId ?? uid()
+  const id = pendingId ?? uuid()
 
   const start = now()
   const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })

+ 78 - 0
packages/app/src/utils/uuid.test.ts

@@ -0,0 +1,78 @@
+import { afterEach, describe, expect, test } from "bun:test"
+import { uuid } from "./uuid"
+
+const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto")
+const secureDescriptor = Object.getOwnPropertyDescriptor(globalThis, "isSecureContext")
+const randomDescriptor = Object.getOwnPropertyDescriptor(Math, "random")
+
+const setCrypto = (value: Partial<Crypto>) => {
+  Object.defineProperty(globalThis, "crypto", {
+    configurable: true,
+    value: value as Crypto,
+  })
+}
+
+const setSecure = (value: boolean) => {
+  Object.defineProperty(globalThis, "isSecureContext", {
+    configurable: true,
+    value,
+  })
+}
+
+const setRandom = (value: () => number) => {
+  Object.defineProperty(Math, "random", {
+    configurable: true,
+    value,
+  })
+}
+
+afterEach(() => {
+  if (cryptoDescriptor) {
+    Object.defineProperty(globalThis, "crypto", cryptoDescriptor)
+  }
+
+  if (secureDescriptor) {
+    Object.defineProperty(globalThis, "isSecureContext", secureDescriptor)
+  }
+
+  if (!secureDescriptor) {
+    delete (globalThis as { isSecureContext?: boolean }).isSecureContext
+  }
+
+  if (randomDescriptor) {
+    Object.defineProperty(Math, "random", randomDescriptor)
+  }
+})
+
+describe("uuid", () => {
+  test("uses randomUUID in secure contexts", () => {
+    setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
+    setSecure(true)
+    expect(uuid()).toBe("00000000-0000-0000-0000-000000000000")
+  })
+
+  test("falls back in insecure contexts", () => {
+    setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
+    setSecure(false)
+    setRandom(() => 0.5)
+    expect(uuid()).toBe("8")
+  })
+
+  test("falls back when randomUUID throws", () => {
+    setCrypto({
+      randomUUID: () => {
+        throw new DOMException("Failed", "OperationError")
+      },
+    })
+    setSecure(true)
+    setRandom(() => 0.5)
+    expect(uuid()).toBe("8")
+  })
+
+  test("falls back when randomUUID is unavailable", () => {
+    setCrypto({})
+    setSecure(true)
+    setRandom(() => 0.5)
+    expect(uuid()).toBe("8")
+  })
+})

+ 12 - 0
packages/app/src/utils/uuid.ts

@@ -0,0 +1,12 @@
+const fallback = () => Math.random().toString(16).slice(2)
+
+export function uuid() {
+  const c = globalThis.crypto
+  if (!c || typeof c.randomUUID !== "function") return fallback()
+  if (typeof globalThis.isSecureContext === "boolean" && !globalThis.isSecureContext) return fallback()
+  try {
+    return c.randomUUID()
+  } catch {
+    return fallback()
+  }
+}