Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export {
} from './utils/misc';
export { isNodeEnv, loadModule } from './utils/node';
export { normalize, normalizeToSize, normalizeUrlToBase } from './utils/normalize';
export { setNormalizationDepthOverrideHint, setSkipNormalizationHint } from './utils/normalizationHints';
export {
addNonEnumerableProperty,
convertToPlainObject,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/integrations/extraerrordata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { IntegrationFn } from '../types-hoist/integration';
import { debug } from '../utils/debug-logger';
import { isError, isPlainObject } from '../utils/is';
import { normalize } from '../utils/normalize';
import { addNonEnumerableProperty } from '../utils/object';
import { setSkipNormalizationHint } from '../utils/normalizationHints';
import { truncate } from '../utils/string';

const INTEGRATION_NAME = 'ExtraErrorData';
Expand Down Expand Up @@ -66,7 +66,7 @@ function _enhanceEventWithErrorData(
if (isPlainObject(normalizedErrorData)) {
// We mark the error data as "already normalized" here, because we don't want other normalization procedures to
// potentially truncate the data we just already normalized, with a certain depth setting.
addNonEnumerableProperty(normalizedErrorData, '__sentry_skip_normalization__', true);
setSkipNormalizationHint(normalizedErrorData);
contexts[exceptionName] = normalizedErrorData;
}

Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { captureException } from './exports';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from './semanticAttributes';
import { startSpanManual } from './tracing';
import { normalize } from './utils/normalize';
import { addNonEnumerableProperty } from './utils/object';
import { setNormalizationDepthOverrideHint } from './utils/normalizationHints';

interface SentryTrpcMiddlewareOptions {
/** Whether to include procedure inputs in reported events. Defaults to `false`. */
Expand Down Expand Up @@ -53,9 +53,8 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) {
procedure_type: type,
};

addNonEnumerableProperty(
setNormalizationDepthOverrideHint(
trpcContext,
'__sentry_override_normalization_depth__',
1 + // 1 for context.input + the normal normalization depth
(clientOptions?.normalizeDepth ?? 5), // 5 is a sane depth
);
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/utils/normalizationHints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { addNonEnumerableProperty } from './object';

/**
* Internal symbols for normalization behavior. JSON and other structured user payloads cannot
* carry these keys, so they cannot spoof SDK-only normalization hints.
*/
const SENTRY_SKIP_NORMALIZATION = Symbol('sentry.skipNormalization');
const SENTRY_OVERRIDE_NORMALIZATION_DEPTH = Symbol('sentry.overrideNormalizationDepth');

/** Marks an object so `normalize` returns it unchanged (already-normalized SDK data). */
export function setSkipNormalizationHint(obj: object): void {
addNonEnumerableProperty(obj, SENTRY_SKIP_NORMALIZATION, true);
}

/** Overrides remaining normalization depth from this object downward (e.g. Redux / Pinia state). */
export function setNormalizationDepthOverrideHint(obj: object, depth: number): void {
addNonEnumerableProperty(obj, SENTRY_OVERRIDE_NORMALIZATION_DEPTH, depth);
}

/** @internal */
export function hasSkipNormalizationHint(value: object) {
return Boolean((value as Record<symbol, unknown>)[SENTRY_SKIP_NORMALIZATION]);
}

/** @internal */
export function getNormalizationDepthOverrideHint(value: object): number | undefined {
const v = (value as Record<symbol, unknown>)[SENTRY_OVERRIDE_NORMALIZATION_DEPTH];
return typeof v === 'number' ? v : undefined;
}
18 changes: 7 additions & 11 deletions packages/core/src/utils/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Primitive } from '../types-hoist/misc';
import { isSyntheticEvent, isVueViewModel } from './is';
import { getNormalizationDepthOverrideHint, hasSkipNormalizationHint } from './normalizationHints';
import { convertToPlainObject } from './object';
import { getFunctionName, getVueInternalName } from './stacktrace';

Expand Down Expand Up @@ -101,20 +102,15 @@ function visit(

// From here on, we can assert that `value` is either an object or an array.

// Do not normalize objects that we know have already been normalized. As a general rule, the
// "__sentry_skip_normalization__" property should only be used sparingly and only should only be set on objects that
// have already been normalized.
if ((value as ObjOrArray<unknown>)['__sentry_skip_normalization__']) {
// Do not normalize objects that we know have already been normalized. Hints use internal symbols
// (see normalizationHints.ts) so user-controlled JSON cannot spoof them.
if (hasSkipNormalizationHint(value)) {
return value as ObjOrArray<unknown>;
}

// We can set `__sentry_override_normalization_depth__` on an object to ensure that from there
// We keep a certain amount of depth.
// This should be used sparingly, e.g. we use it for the redux integration to ensure we get a certain amount of state.
const remainingDepth =
typeof (value as ObjOrArray<unknown>)['__sentry_override_normalization_depth__'] === 'number'
? ((value as ObjOrArray<unknown>)['__sentry_override_normalization_depth__'] as number)
: depth;
// Override remaining depth from this node (e.g. Redux / Pinia state). Set via setNormalizationDepthOverrideHint.
const overrideDepth = getNormalizationDepthOverrideHint(value);
const remainingDepth = overrideDepth !== undefined ? overrideDepth : depth;

// We're also done if we've reached the max depth
if (remainingDepth === 0) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/utils/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa
* @param name The name of the property to be set
* @param value The value to which to set the property
*/
export function addNonEnumerableProperty(obj: object, name: string, value: unknown): void {
export function addNonEnumerableProperty(obj: object, name: string | symbol, value: unknown): void {
Comment thread
cursor[bot] marked this conversation as resolved.
try {
Object.defineProperty(obj, name, {
// enumerable: false, // the default, so we can save on bundle size by not explicitly setting it
Expand All @@ -62,7 +62,7 @@ export function addNonEnumerableProperty(obj: object, name: string, value: unkno
configurable: true,
});
} catch {
DEBUG_BUILD && debug.log(`Failed to add non-enumerable property "${name}" to object`, obj);
DEBUG_BUILD && debug.log(`Failed to add non-enumerable property "${String(name)}" to object`, obj);
}
}

Expand Down
35 changes: 28 additions & 7 deletions packages/core/test/lib/utils/normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import { describe, expect, test, vi } from 'vitest';
import { addNonEnumerableProperty, normalize } from '../../../src';
import { normalize, setNormalizationDepthOverrideHint, setSkipNormalizationHint } from '../../../src';
import * as isModule from '../../../src/utils/is';
import * as stacktraceModule from '../../../src/utils/stacktrace';

Expand Down Expand Up @@ -655,15 +655,36 @@ describe('normalize()', () => {
});
});

describe('skips normalizing objects marked with a non-enumerable property __sentry_skip_normalization__', () => {
describe('regression: JSON cannot spoof skip-normalization via string keys', () => {
test('__sentry_skip_normalization__ as an own string property is still normalized', () => {
function someFun(): void {
/* no-empty */
}
const jsonLikePayload = {
__sentry_skip_normalization__: true,
nan: NaN,
fun: someFun,
};

const result = normalize(jsonLikePayload);

expect(result).toEqual({
__sentry_skip_normalization__: true,
nan: '[NaN]',
fun: '[Function: someFun]',
});
});
});

describe('skips normalizing objects marked with setSkipNormalizationHint (internal symbol)', () => {
test('by leaving non-serializable values intact', () => {
const someFun = () => undefined;
const alreadyNormalizedObj = {
nan: NaN,
fun: someFun,
};

addNonEnumerableProperty(alreadyNormalizedObj, '__sentry_skip_normalization__', true);
setSkipNormalizationHint(alreadyNormalizedObj);

const result = normalize(alreadyNormalizedObj);
expect(result).toEqual({
Expand All @@ -681,7 +702,7 @@ describe('normalize()', () => {
},
};

addNonEnumerableProperty(alreadyNormalizedObj, '__sentry_skip_normalization__', true);
setSkipNormalizationHint(alreadyNormalizedObj);

const obj = {
foo: {
Expand All @@ -703,7 +724,7 @@ describe('normalize()', () => {
});
});

describe('overrides normalization depth with a non-enumerable property __sentry_override_normalization_depth__', () => {
describe('overrides normalization depth with setNormalizationDepthOverrideHint', () => {
test('by increasing depth if it is higher', () => {
const normalizationTarget = {
foo: 'bar',
Expand All @@ -717,7 +738,7 @@ describe('normalize()', () => {
},
};

addNonEnumerableProperty(normalizationTarget, '__sentry_override_normalization_depth__', 3);
setNormalizationDepthOverrideHint(normalizationTarget, 3);

const result = normalize(normalizationTarget, 1);

Expand Down Expand Up @@ -745,7 +766,7 @@ describe('normalize()', () => {
},
};

addNonEnumerableProperty(normalizationTarget, '__sentry_override_normalization_depth__', 1);
setNormalizationDepthOverrideHint(normalizationTarget, 1);
Comment thread
cursor[bot] marked this conversation as resolved.

const result = normalize(normalizationTarget, 3);

Expand Down
11 changes: 8 additions & 3 deletions packages/react/src/redux.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Scope } from '@sentry/core';
import { addBreadcrumb, addNonEnumerableProperty, getClient, getCurrentScope, getGlobalScope } from '@sentry/core';
import {
addBreadcrumb,
getClient,
getCurrentScope,
getGlobalScope,
setNormalizationDepthOverrideHint,
} from '@sentry/core';

interface Action<T = any> {
type: T;
Expand Down Expand Up @@ -138,9 +144,8 @@ function createReduxEnhancer(enhancerOptions?: Partial<SentryEnhancerOptions>):

// Set the normalization depth of the redux state to the configured `normalizeDepth` option or a sane number as a fallback
const newStateContext = { state: { type: 'redux', value: transformedState } };
addNonEnumerableProperty(
setNormalizationDepthOverrideHint(
newStateContext,
'__sentry_override_normalization_depth__',
3 + // 3 layers for `state.value.transformedState`
normalizationDepth, // rest for the actual state
);
Expand Down
11 changes: 8 additions & 3 deletions packages/vue/src/pinia.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { addBreadcrumb, addNonEnumerableProperty, getClient, getCurrentScope, getGlobalScope } from '@sentry/core';
import {
addBreadcrumb,
getClient,
getCurrentScope,
getGlobalScope,
setNormalizationDepthOverrideHint,
} from '@sentry/core';
import type { Ref } from 'vue';

// Inline Pinia types
Expand Down Expand Up @@ -112,9 +118,8 @@ export const createSentryPiniaPlugin: (
state: piniaStateContext,
};

addNonEnumerableProperty(
setNormalizationDepthOverrideHint(
newState,
'__sentry_override_normalization_depth__',
3 + // 3 layers for `state.value.transformedState
normalizationDepth, // rest for the actual state
);
Expand Down
Loading