Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/secure-session-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tindalabs/blindspot-core": patch
---

Session id generation no longer falls back to `Math.random()`. It now prefers `crypto.randomUUID`, then a `crypto.getRandomValues`-based v4 UUID, leaving only a non-random monotonic id (`bs-<ts>-<n>`) as a last resort for runtimes with no Web Crypto at all. Resolves CodeQL `js/insecure-randomness`. The session id is a UX-telemetry marker rather than a security token, but there's no reason to keep weak randomness in the SDK.
2 changes: 1 addition & 1 deletion e2e/tests/next.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ test.describe('next-app-router — network spans', () => {
await page.goto('/');

await Promise.all([
page.waitForResponse(/jsonplaceholder\.typicode\.com/),
page.waitForResponse(/^https:\/\/jsonplaceholder\.typicode\.com\//),
page.click('[data-blindspot-label="fetch-task-data"]'),
]);
await flushSpans(page);
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/react.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ test.describe('react-basic — network spans', () => {

// Click "Fetch task data" and wait for the mocked response.
await Promise.all([
page.waitForResponse(/jsonplaceholder\.typicode\.com/),
page.waitForResponse(/^https:\/\/jsonplaceholder\.typicode\.com\//),
page.click('button.btn-primary'),
]);
await flushSpans(page);
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/vue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ test.describe('vue-basic — network spans', () => {

// Click "Fetch task data" (Vue has a data-blindspot-label on this button).
await Promise.all([
page.waitForResponse(/jsonplaceholder\.typicode\.com/),
page.waitForResponse(/^https:\/\/jsonplaceholder\.typicode\.com\//),
page.click('[data-blindspot-label="fetch-task-data"]'),
]);
await flushSpans(page);
Expand Down
28 changes: 21 additions & 7 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
const SESSION_KEY = 'blindspot.session_id';

function generateId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
if (typeof crypto !== 'undefined') {
if (typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
// Fallback for environments without crypto.randomUUID: build a v4 UUID from
// crypto.getRandomValues, which is cryptographically secure and has far
// wider support than randomUUID. Math.random is intentionally avoided.
if (typeof crypto.getRandomValues === 'function') {
const bytes = crypto.getRandomValues(new Uint8Array(16));
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
}
// Fallback for environments without crypto.randomUUID
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
// Last resort for the rare runtime with no Web Crypto at all. This id is a
// UX-telemetry session marker, not a security token, so a non-random
// monotonic fallback is acceptable here.
_fallbackCounter += 1;
return `bs-${Date.now().toString(36)}-${_fallbackCounter.toString(36)}`;
}

let _fallbackCounter = 0;

export function getOrCreateSessionId(): string {
try {
const existing = sessionStorage.getItem(SESSION_KEY);
Expand Down