Skip to content

Commit bb425b3

Browse files
committed
fix(shared): keep protect-check sdk_url out of the load-failure error
The dynamic-import failure message embeds the sdk_url in Chromium/Firefox; stop interpolating it into the user-facing ClerkRuntimeError, and make the test assert the invariant rather than relying on Node's URL-free import error.
1 parent 3963323 commit bb425b3

2 files changed

Lines changed: 11 additions & 9 deletions

File tree

packages/shared/src/internal/clerk-js/__tests__/protectCheck.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,20 @@ describe('executeProtectCheck', () => {
153153
});
154154
});
155155

156-
it('does not leak the sdkUrl in the user-facing load-failure message', async () => {
156+
it('does not append the underlying import error (which can embed the sdkUrl) to the message', async () => {
157+
// Node's import error omits the URL, so the not-toContain checks below are vacuous on their own.
158+
// The `Original error` guard is the real one: a browser embeds the sdk_url in the import failure,
159+
// which must never reach the user-facing message.
157160
try {
158161
await executeProtectCheck(
159162
protectCheck({ sdkUrl: 'https://attacker-controlled.example/evil.js' }),
160163
fakeContainer(),
161164
);
162165
throw new Error('should have rejected');
163166
} catch (err: any) {
167+
expect(err.code).toBe('protect_check_script_load_failed');
168+
expect(err.message).toContain('invalid module.');
169+
expect(err.message).not.toMatch(/original error/i);
164170
expect(err.message).not.toContain('attacker-controlled.example');
165171
expect(err.message).not.toContain('evil.js');
166172
}

packages/shared/src/internal/clerk-js/protectCheck.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,13 @@ export async function executeProtectCheck(
100100
let mod: Record<string, unknown>;
101101
try {
102102
mod = await import(/* webpackIgnore: true */ validated.toString());
103-
} catch (err) {
104-
// The browser surfaces CSP-blocked imports as the same error shape as a network error
105-
// (typically a TypeError "Failed to fetch dynamically imported module"), so we can't
106-
// reliably distinguish them. Surface a generic message to the UI — the URL is NOT
107-
// included to avoid a phishing surface where a tampered response could place an
108-
// attacker-chosen URL in the auth UI. Diagnostic detail goes to the original error.
109-
const original = err instanceof Error ? err.message : String(err);
103+
} catch {
104+
// Surface a generic message and deliberately omit the original error: Chromium/Firefox embed
105+
// the sdk_url in the dynamic-import failure text, which a tampered response could plant in the UI.
110106
throw new ClerkRuntimeError(
111107
'Protect check script failed to load. This is commonly caused by a Content Security ' +
112108
'Policy that blocks the script origin (add it to your script-src directive), a ' +
113-
`network error, or an invalid module. (Original error: ${original})`,
109+
'network error, or an invalid module.',
114110
{ code: 'protect_check_script_load_failed' },
115111
);
116112
}

0 commit comments

Comments
 (0)