From d9eb598a5245173b509620afea10bd0eb6676766 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Jul 2026 09:02:43 +0000
Subject: [PATCH 1/3] Initial plan
From 672b0ca0f632f185524a8e89605a7bed44fc8b0e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Jul 2026 09:04:54 +0000
Subject: [PATCH 2/3] chore: outline plan for highlightSearch xss fix
---
console/pnpm-workspace.yaml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/console/pnpm-workspace.yaml b/console/pnpm-workspace.yaml
index 25c0d18a5..ef75c386c 100644
--- a/console/pnpm-workspace.yaml
+++ b/console/pnpm-workspace.yaml
@@ -1,3 +1,6 @@
+allowBuilds:
+ '@clerk/shared': set this to true or false
+ esbuild: set this to true or false
onlyBuiltDependencies:
- '@clerk/shared'
- esbuild
From 83db70d63aa5fea6e3e0bd31c9be4f5b225b260b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Jul 2026 09:07:38 +0000
Subject: [PATCH 3/3] fix(console): prevent xss in highlightSearch rendering
---
console/src/lib/ui-utils.test.tsx | 26 ++++++++++++++++++
console/src/lib/ui-utils.tsx | 27 +++++++++++++++----
.../src/views/users/rules/FilterRuleEdit.tsx | 8 +-----
.../src/views/users/rules/JourneyRuleEdit.tsx | 8 +-----
.../views/users/rules/MemberConditionEdit.tsx | 10 +++----
.../src/views/users/rules/RuleEventName.tsx | 8 +-----
.../users/rules/RuleOrganizationEventName.tsx | 8 +-----
7 files changed, 55 insertions(+), 40 deletions(-)
create mode 100644 console/src/lib/ui-utils.test.tsx
diff --git a/console/src/lib/ui-utils.test.tsx b/console/src/lib/ui-utils.test.tsx
new file mode 100644
index 000000000..3586d1610
--- /dev/null
+++ b/console/src/lib/ui-utils.test.tsx
@@ -0,0 +1,26 @@
+import { render, screen } from "@testing-library/react"
+import { highlightSearch } from "./ui-utils"
+
+describe("highlightSearch", () => {
+ it("highlights all exact matches with strong tags", () => {
+ render(
{highlightSearch("foo foo", "foo")}
)
+
+ expect(screen.getAllByText("foo", { selector: "strong" })).toHaveLength(2)
+ })
+
+ it("does not render HTML from untrusted input", () => {
+ const payload = `
`
+ const { container } = render({highlightSearch(payload, "img")}
)
+
+ expect(container.querySelector("img")).toBeNull()
+ expect(screen.getByText("img", { selector: "strong" })).toBeInTheDocument()
+ expect(container).toHaveTextContent(payload)
+ })
+
+ it("returns plain text when search is empty", () => {
+ const { container } = render({highlightSearch("plain text", "")}
)
+
+ expect(screen.getByText("plain text")).toBeInTheDocument()
+ expect(container.querySelector("strong")).toBeNull()
+ })
+})
diff --git a/console/src/lib/ui-utils.tsx b/console/src/lib/ui-utils.tsx
index 071f7a1d2..c4136e2fa 100644
--- a/console/src/lib/ui-utils.tsx
+++ b/console/src/lib/ui-utils.tsx
@@ -1,4 +1,4 @@
-import type { Key } from "react"
+import type { Key, ReactNode } from "react"
import { useState } from "react"
import type { Modifier } from "react-popper"
import { usePopper } from "react-popper"
@@ -61,10 +61,27 @@ export const defaultGetOptionDisplay = (option: unknown) =>
? ((option as Record).label ?? (option as Record).name)
: option) as string
-export const highlightSearch = (text: string, search: string, matchClassName = "match") =>
- text && search
- ? text.replaceAll(search, `$&`)
- : (text ?? "")
+export const highlightSearch = (
+ text: string,
+ search: string,
+ matchClassName = "match",
+): ReactNode => {
+ if (!text || !search) return text ?? ""
+
+ const parts = text.split(search)
+ if (parts.length === 1) return text
+
+ return parts.flatMap((part, index) =>
+ index < parts.length - 1
+ ? [
+ part,
+
+ {search}
+ ,
+ ]
+ : [part],
+ )
+}
/**
* Navigate from inside a Radix-based overlay (Sheet, Dialog, DropdownMenu).
diff --git a/console/src/views/users/rules/FilterRuleEdit.tsx b/console/src/views/users/rules/FilterRuleEdit.tsx
index c69a7388f..8f1f56a45 100644
--- a/console/src/views/users/rules/FilterRuleEdit.tsx
+++ b/console/src/views/users/rules/FilterRuleEdit.tsx
@@ -123,13 +123,7 @@ export default function FilterRuleEdit({
required
inputClassName="rounded-none border-l-0"
buttonClassName="rounded-none"
- renderOption={(option, search) => (
-
- )}
+ renderOption={(option, search) => highlightSearch(option.path, search)}
/>
diff --git a/console/src/views/users/rules/JourneyRuleEdit.tsx b/console/src/views/users/rules/JourneyRuleEdit.tsx
index 8c3feb012..38fd7e27e 100644
--- a/console/src/views/users/rules/JourneyRuleEdit.tsx
+++ b/console/src/views/users/rules/JourneyRuleEdit.tsx
@@ -103,13 +103,7 @@ export default function JourneyRuleEdit({
required
inputClassName="rounded-none border-l-0"
buttonClassName="rounded-none"
- renderOption={(option, search) => (
-
- )}
+ renderOption={(option, search) => highlightSearch(option.path, search)}
/>
diff --git a/console/src/views/users/rules/MemberConditionEdit.tsx b/console/src/views/users/rules/MemberConditionEdit.tsx
index 462157a47..e61af9a29 100644
--- a/console/src/views/users/rules/MemberConditionEdit.tsx
+++ b/console/src/views/users/rules/MemberConditionEdit.tsx
@@ -113,13 +113,9 @@ export default function MemberConditionEdit({
required
inputClassName="rounded-none border-l-0"
buttonClassName="rounded-none"
- renderOption={(option, search) => (
-
- )}
+ renderOption={(option, search) =>
+ highlightSearch(option.name || option.path, search)
+ }
/>