Skip to content

Commit a2e0c40

Browse files
authored
Merge pull request #150 from codex-team/fix/prevent-invalid-payload-from-beforeSend
fix(catchers): validate beforeSend return value to avoid sending invalid payload
2 parents b4d9c65 + d2b0812 commit a2e0c40

File tree

18 files changed

+1195
-52
lines changed

18 files changed

+1195
-52
lines changed

.github/workflows/main.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,24 @@ jobs:
1616
- run: yarn install
1717
- run: yarn lint-test
1818

19-
build:
19+
test:
2020
runs-on: ubuntu-latest
2121
env:
2222
CI_JOB_NUMBER: 2
23+
steps:
24+
- uses: actions/checkout@v1
25+
- name: Use Node.js from .nvmrc
26+
uses: actions/setup-node@v6
27+
with:
28+
node-version-file: '.nvmrc'
29+
- run: corepack enable
30+
- run: yarn install
31+
- run: yarn workspace @hawk.so/javascript test
32+
33+
build:
34+
runs-on: ubuntu-latest
35+
env:
36+
CI_JOB_NUMBER: 3
2337
steps:
2438
- uses: actions/checkout@v1
2539
with:

packages/javascript/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Initialization settings:
8989
| `disableVueErrorHandler` | boolean | optional | Do not initialize Vue errors handling |
9090
| `consoleTracking` | boolean | optional | Initialize console logs tracking |
9191
| `breadcrumbs` | false or BreadcrumbsOptions object | optional | Configure breadcrumbs tracking (see below) |
92-
| `beforeSend` | function(event) => event | optional | This Method allows you to filter any data you don't want sending to Hawk |
92+
| `beforeSend` | function(event) => event \| false \| void | optional | Filter data before sending. Return modified event, `false` to drop the event. |
9393

9494
Other available [initial settings](types/hawk-initial-settings.d.ts) are described at the type definition.
9595

@@ -187,7 +187,7 @@ const hawk = new HawkCatcher({
187187
beforeBreadcrumb: (breadcrumb, hint) => {
188188
// Filter or modify breadcrumbs before storing
189189
if (breadcrumb.category === 'fetch' && breadcrumb.data?.url?.includes('/sensitive')) {
190-
return null; // Discard this breadcrumb
190+
return false; // Discard this breadcrumb
191191
}
192192
return breadcrumb;
193193
}
@@ -203,7 +203,7 @@ const hawk = new HawkCatcher({
203203
| `trackFetch` | `boolean` | `true` | Automatically track `fetch()` and `XMLHttpRequest` calls as breadcrumbs. Captures request URL, method, status code, and response time. |
204204
| `trackNavigation` | `boolean` | `true` | Automatically track navigation events (History API: `pushState`, `replaceState`, `popstate`). Captures route changes. |
205205
| `trackClicks` | `boolean` | `true` | Automatically track UI click events. Captures element selector, coordinates, and other click metadata. |
206-
| `beforeBreadcrumb` | `function` | `undefined` | Hook called before each breadcrumb is stored. Receives `(breadcrumb, hint)` and can return modified breadcrumb, `null` to discard it, or the original breadcrumb. Useful for filtering sensitive data or PII. |
206+
| `beforeBreadcrumb` | `function` | `undefined` | Hook called before each breadcrumb is stored. Receives `(breadcrumb, hint)`. Return modified breadcrumb to keep it, `false` to discard. |
207207

208208
### Manual Breadcrumbs
209209

packages/javascript/example/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ <h2>Test Vue integration: &lt;test-component&gt;</h2>
312312
// beforeBreadcrumb: (breadcrumb, hint) => {
313313
// // Filter or modify breadcrumbs before storing
314314
// if (breadcrumb.category === 'fetch' && breadcrumb.data?.url?.includes('/sensitive')) {
315-
// return null; // Discard this breadcrumb
315+
// return false; // Discard this breadcrumb
316316
// }
317317
// return breadcrumb;
318318
// }

packages/javascript/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hawk.so/javascript",
3-
"version": "3.2.15",
3+
"version": "3.2.16",
44
"description": "JavaScript errors tracking for Hawk.so",
55
"files": [
66
"dist"
@@ -20,6 +20,8 @@
2020
"dev": "vite",
2121
"build": "vite build",
2222
"stats": "size-limit > stats.txt",
23+
"test": "vitest run",
24+
"test:watch": "vitest",
2325
"lint": "eslint --fix \"src/**/*.{js,ts}\""
2426
},
2527
"repository": {
@@ -40,9 +42,11 @@
4042
"error-stack-parser": "^2.1.4"
4143
},
4244
"devDependencies": {
43-
"@hawk.so/types": "0.5.2",
45+
"@hawk.so/types": "0.5.8",
46+
"jsdom": "^28.0.0",
4447
"vite": "^7.3.1",
4548
"vite-plugin-dts": "^4.2.4",
49+
"vitest": "^4.0.18",
4650
"vue": "^2"
4751
}
4852
}

packages/javascript/src/addons/breadcrumbs.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Breadcrumb, BreadcrumbLevel, BreadcrumbType, Json, JsonNode } from
55
import Sanitizer from '../modules/sanitizer';
66
import { buildElementSelector } from '../utils/selector';
77
import log from '../utils/log';
8+
import { isValidBreadcrumb } from '../utils/validation';
89

910
/**
1011
* Default maximum number of breadcrumbs to store
@@ -48,11 +49,13 @@ export interface BreadcrumbsOptions {
4849
maxBreadcrumbs?: number;
4950

5051
/**
51-
* Hook called before each breadcrumb is stored
52-
* Return null to discard the breadcrumb
53-
* Return modified breadcrumb to store it
52+
* Hook called before each breadcrumb is stored.
53+
* - Return modified breadcrumb — it will be stored instead of the original.
54+
* - Return `false` — the breadcrumb will be discarded.
55+
* - Return nothing (`void` / `undefined` / `null`) — the original breadcrumb is stored as-is (a warning is logged).
56+
* - If the hook returns an invalid value, a warning is logged and the original breadcrumb is stored.
5457
*/
55-
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null;
58+
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | void;
5659

5760
/**
5861
* Enable automatic fetch/XHR breadcrumbs
@@ -91,7 +94,7 @@ interface InternalBreadcrumbsOptions {
9194
trackFetch: boolean;
9295
trackNavigation: boolean;
9396
trackClicks: boolean;
94-
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null;
97+
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | void;
9598
}
9699

97100
/**
@@ -233,16 +236,30 @@ export class BreadcrumbManager {
233236
* Apply beforeBreadcrumb hook
234237
*/
235238
if (this.options.beforeBreadcrumb) {
236-
const result = this.options.beforeBreadcrumb(bc, hint);
239+
const breadcrumbClone = structuredClone(bc);
240+
const result = this.options.beforeBreadcrumb(breadcrumbClone, hint);
237241

238-
if (result === null) {
239-
/**
240-
* Discard breadcrumb
241-
*/
242+
/**
243+
* false means discard
244+
*/
245+
if (result === false) {
242246
return;
243247
}
244248

245-
Object.assign(bc, result);
249+
/**
250+
* Valid breadcrumb → apply changes from hook
251+
*/
252+
if (isValidBreadcrumb(result)) {
253+
Object.assign(bc, result);
254+
} else {
255+
/**
256+
* Anything else is invalid — warn, bc stays untouched (hook only received a clone)
257+
*/
258+
log(
259+
'Invalid beforeBreadcrumb value. It should return breadcrumb or false. Breadcrumb is stored without changes.',
260+
'warn'
261+
);
262+
}
246263
}
247264

248265
/**

packages/javascript/src/addons/consoleCatcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export class ConsoleCatcher {
150150
* This ensures DevTools will navigate to the user's code, not the interceptor's code.
151151
*
152152
* @param errorStack - Full stack trace string from Error.stack
153-
* @returns Object with userStack (full stack from user code) and fileLine (first frame for DevTools link)
153+
* @returns {object} Object with userStack (full stack from user code) and fileLine (first frame for DevTools link)
154154
*/
155155
private extractUserStack(errorStack: string | undefined): {
156156
userStack: string;
@@ -250,7 +250,7 @@ export class ConsoleCatcher {
250250
* 4. Store it in the buffer
251251
* 5. Forward the call to the native console (so output still appears in DevTools)
252252
*
253-
* @param {...any} args
253+
* @param args - console method arguments
254254
*/
255255
window.console[method] = (...args: unknown[]): void => {
256256
// Capture full stack trace and extract user code stack

packages/javascript/src/catcher.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@ import Socket from './modules/socket';
22
import Sanitizer from './modules/sanitizer';
33
import log from './utils/log';
44
import StackParser from './modules/stackParser';
5-
import type { CatcherMessage, HawkInitialSettings, BreadcrumbsAPI } from './types';
5+
import type { CatcherMessage, HawkInitialSettings, BreadcrumbsAPI, Transport } from './types';
66
import { VueIntegration } from './integrations/vue';
77
import { id } from './utils/id';
88
import type {
99
AffectedUser,
1010
EventContext,
1111
JavaScriptAddons,
1212
VueIntegrationAddons,
13-
Json, EncodedIntegrationToken, DecodedIntegrationToken,
13+
Json, EncodedIntegrationToken, DecodedIntegrationToken
1414
} from '@hawk.so/types';
1515
import type { JavaScriptCatcherIntegrations } from './types/integrations';
1616
import { EventRejectedError } from './errors';
1717
import type { HawkJavaScriptEvent } from './types';
1818
import { isErrorProcessed, markErrorAsProcessed } from './utils/event';
1919
import { ConsoleCatcher } from './addons/consoleCatcher';
2020
import { BreadcrumbManager } from './addons/breadcrumbs';
21-
import { validateUser, validateContext } from './utils/validation';
21+
import { validateUser, validateContext, isValidEventPayload } from './utils/validation';
2222

2323
/**
2424
* Allow to use global VERSION, that will be overwritten by Webpack
@@ -73,16 +73,18 @@ export default class Catcher {
7373
private context: EventContext | undefined;
7474

7575
/**
76-
* This Method allows developer to filter any data you don't want sending to Hawk
77-
* If method returns false, event will not be sent
76+
* This Method allows developer to filter any data you don't want sending to Hawk.
77+
* - Return modified event — it will be sent instead of the original.
78+
* - Return `false` — the event will be dropped entirely.
79+
* - Any other value is invalid — the original event is sent as-is (a warning is logged).
7880
*/
79-
private readonly beforeSend: undefined | ((event: HawkJavaScriptEvent) => HawkJavaScriptEvent | false);
81+
private readonly beforeSend: undefined | ((event: HawkJavaScriptEvent) => HawkJavaScriptEvent | false | void);
8082

8183
/**
8284
* Transport for dialog between Catcher and Collector
83-
* (WebSocket decorator)
85+
* (WebSocket decorator by default, or custom via settings.transport)
8486
*/
85-
private readonly transport: Socket;
87+
private readonly transport: Transport;
8688

8789
/**
8890
* Module for parsing backtrace
@@ -148,7 +150,7 @@ export default class Catcher {
148150
/**
149151
* Init transport
150152
*/
151-
this.transport = new Socket({
153+
this.transport = settings.transport ?? new Socket({
152154
collectorEndpoint: settings.collectorEndpoint || `wss://${this.getIntegrationId()}.k1.hawk.so:443/ws`,
153155
reconnectionAttempts: settings.reconnectionAttempts,
154156
reconnectionTimeout: settings.reconnectionTimeout,
@@ -436,12 +438,29 @@ export default class Catcher {
436438
* Filter sensitive data
437439
*/
438440
if (typeof this.beforeSend === 'function') {
439-
const beforeSendResult = this.beforeSend(payload);
441+
const eventPayloadClone = structuredClone(payload);
442+
const result = this.beforeSend(eventPayloadClone);
440443

441-
if (beforeSendResult === false) {
444+
/**
445+
* false → drop event
446+
*/
447+
if (result === false) {
442448
throw new EventRejectedError('Event rejected by beforeSend method.');
449+
}
450+
451+
/**
452+
* Valid event payload → use it instead of original
453+
*/
454+
if (isValidEventPayload(result)) {
455+
payload = result as HawkJavaScriptEvent;
443456
} else {
444-
payload = beforeSendResult;
457+
/**
458+
* Anything else is invalid — warn, payload stays untouched (hook only received a clone)
459+
*/
460+
log(
461+
'Invalid beforeSend value. It should return event or false. Event is sent without changes.',
462+
'warn'
463+
);
445464
}
446465
}
447466

packages/javascript/src/modules/socket.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import log from '../utils/log';
22
import type { CatcherMessage } from '@/types';
3+
import type { Transport } from '../types/transport';
34

45
/**
56
* Custom WebSocket wrapper class
67
*
78
* @copyright CodeX
89
*/
9-
export default class Socket {
10+
export default class Socket implements Transport {
1011
/**
1112
* Socket connection endpoint
1213
*/

packages/javascript/src/types/hawk-initial-settings.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { EventContext, AffectedUser } from '@hawk.so/types';
22
import type { HawkJavaScriptEvent } from './event';
3+
import type { Transport } from './transport';
34
import type { BreadcrumbsOptions } from '../addons/breadcrumbs';
45

56
/**
@@ -65,10 +66,11 @@ export interface HawkInitialSettings {
6566

6667
/**
6768
* This Method allows you to filter any data you don't want sending to Hawk.
68-
*
69-
* Return `false` to prevent the event from being sent to Hawk.
69+
* - Return modified event — it will be sent instead of the original.
70+
* - Return `false` — the event will be dropped entirely.
71+
* - Any other value is invalid — the original event is sent as-is (a warning is logged).
7072
*/
71-
beforeSend?(event: HawkJavaScriptEvent): HawkJavaScriptEvent | false;
73+
beforeSend?(event: HawkJavaScriptEvent): HawkJavaScriptEvent | false | void;
7274

7375
/**
7476
* Disable Vue.js error handler
@@ -90,4 +92,10 @@ export interface HawkInitialSettings {
9092
* @default enabled with default options
9193
*/
9294
breadcrumbs?: false | BreadcrumbsOptions;
95+
96+
/**
97+
* Custom transport for sending events.
98+
* If not provided, default WebSocket transport is used.
99+
*/
100+
transport?: Transport;
93101
}

packages/javascript/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { CatcherMessage } from './catcher-message';
22
import type { HawkInitialSettings } from './hawk-initial-settings';
3+
import type { Transport } from './transport';
34
import type { HawkJavaScriptEvent } from './event';
45
import type { VueIntegrationData, NuxtIntegrationData, NuxtIntegrationAddons, JavaScriptCatcherIntegrations } from './integrations';
56
import type { BreadcrumbsAPI } from './breadcrumbs-api';
67

78
export type {
89
CatcherMessage,
910
HawkInitialSettings,
11+
Transport,
1012
HawkJavaScriptEvent,
1113
VueIntegrationData,
1214
NuxtIntegrationData,

0 commit comments

Comments
 (0)