From 2910d5ad6a959c6a4f36337cefdc8e5ff766c036 Mon Sep 17 00:00:00 2001 From: Mark Jubenville Date: Sun, 17 May 2026 12:17:41 -0400 Subject: [PATCH 1/3] fix(279): avoid leaking sanitization error payloads --- README.md | 5 +- docs/plans/003-sanitization-error-details.md | 44 +++++++++++++++ src/errors.ts | 4 +- src/index.ts | 55 ++++++++++++++++++- test/index-errors.test.ts | 58 +++++++++++++++++++- 5 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 docs/plans/003-sanitization-error-details.md diff --git a/README.md b/README.md index 9335384..0e345e9 100644 --- a/README.md +++ b/README.md @@ -148,11 +148,14 @@ try { } catch (error) { if (error instanceof DataSanitizationError) { console.error(error.message); // 'Invalid data type' - console.error(error.details); // { originalData: 123 } + console.error(error.details); // { inputType: 'number' } } } ``` +Error details are limited to safe diagnostic metadata and do not include the +original input payload. + ## How it works 1. **String input** is sanitized directly via regex replacement. diff --git a/docs/plans/003-sanitization-error-details.md b/docs/plans/003-sanitization-error-details.md new file mode 100644 index 0000000..85c8d7e --- /dev/null +++ b/docs/plans/003-sanitization-error-details.md @@ -0,0 +1,44 @@ +# Sanitization Error Details + +## Approach + +Update `sanitizeData` so wrapped sanitization failures expose only safe +diagnostic metadata instead of retaining caller payloads. Keep the public +`DataSanitizationError` shape intact while replacing raw `originalData` entries +with input type and wrapped error metadata that callers can log without leaking +sensitive values. + +## Pre-implementation + +Create issue branch `fix/279/sanitization_error_details` for GitHub issue #279. + +## Steps + +1. Update `src/index.ts` to build safe error details for invalid input and + wrapped failures. +2. Update `test/index-errors.test.ts` with regression coverage for malformed + sanitized JSON and sensitive payload values. +3. Update `README.md` so the error handling example logs safe fields instead of + raw details. + +## Relevant Files + +- `docs/plans/003-sanitization-error-details.md` - new plan for issue #279. +- `src/index.ts` - updated sanitizer error details. +- `test/index-errors.test.ts` - updated regression coverage for safe error + details. +- `README.md` - updated error handling guidance. + +## Verification + +Run the full test suite, linting, and formatting checks through package scripts. + +## Decisions + +- Preserve `DataSanitizationError.details` as a structured object so existing + callers keep a stable error shape. +- Use safe metadata instead of sanitized payload summaries because parse failures + happen when the sanitized payload may be malformed, and summaries can still + reveal application data shape or user content. +- Include the wrapped error name for debugging while excluding raw input, error + objects, messages, and stack traces from public details. diff --git a/src/errors.ts b/src/errors.ts index 56908c9..f598009 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,7 +4,7 @@ * * @example * const error = new DataSanitizationError('Invalid data type', { - * originalData: 123, + * inputType: 'number', * }); * error.name; * // => 'DataSanitizationError' @@ -20,7 +20,7 @@ class DataSanitizationError extends Error { * * @example * const error = new DataSanitizationError('Invalid data type', { - * originalData: 123, + * inputType: 'number', * }); */ constructor(message: string, details: unknown = {}) { diff --git a/src/index.ts b/src/index.ts index 1410a99..09cf7f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,56 @@ import { DataSanitizationError } from './errors'; import { stringReplacer } from './replacers'; import { DataSanitizationReplacer } from './types'; +/** + * Returns a safe type label for data passed to the sanitizer. + * + * @param data - Input value being sanitized. + * @returns A type label that does not expose the input value. + * @throws Does not throw. + * + * @example + * getInputType({ password: 'secret' }) + * // => 'object' + */ +const getInputType = (data: unknown): string => { + if (data === null) { + return 'null'; + } + + if (Array.isArray(data)) { + return 'array'; + } + + return typeof data; +}; + +/** + * Builds public error details that do not include caller payloads. + * + * @param data - Input value being sanitized. + * @param error - Optional wrapped error from the failed operation. + * @returns Structured diagnostics safe for logging. + * @throws Does not throw. + * + * @example + * createSafeErrorDetails({ password: 'secret' }, new SyntaxError('Bad JSON')) + * // => { inputType: 'object', errorName: 'SyntaxError' } + */ +const createSafeErrorDetails = ( + data: unknown, + error?: unknown, +): Record => { + const details: Record = { + inputType: getInputType(data), + }; + + if (error instanceof Error) { + details.errorName = error.name; + } + + return details; +}; + /** * Sanitizes data in an object/string to make it safe for logging * or other purposes of a sensitive nature @@ -49,15 +99,14 @@ const sanitizeData: DataSanitizationReplacer = (data, options = {}) => { } throw new DataSanitizationError('Invalid data type', { - originalData: data, + ...createSafeErrorDetails(data), }); } catch (error) { if (error instanceof DataSanitizationError) { throw error; } throw new DataSanitizationError('Error parsing data', { - error, - originalData: data, + ...createSafeErrorDetails(data, error), }); } }; diff --git a/test/index-errors.test.ts b/test/index-errors.test.ts index 09c9443..f809887 100644 --- a/test/index-errors.test.ts +++ b/test/index-errors.test.ts @@ -115,15 +115,24 @@ describe('DataSanitizationIndexAndErrors', () => { it('should throw DataSanitizationError for invalid data type', () => { // Arrange const input = 123 as unknown as Record; + let thrownError: unknown; // Act const act = (): void => { - sanitizeData(input); + try { + sanitizeData(input); + } catch (error) { + thrownError = error; + throw error; + } }; // Assert expect(act).toThrowError(DataSanitizationError); expect(act).toThrowError('Invalid data type'); + expect((thrownError as DataSanitizationError).details).toEqual({ + inputType: 'number', + }); }); it('should throw DataSanitizationError for boolean input', () => { @@ -156,15 +165,60 @@ describe('DataSanitizationIndexAndErrors', () => { // Arrange const input: Record = {}; input.self = input; + let thrownError: unknown; // Act const act = (): void => { - sanitizeData(input); + try { + sanitizeData(input); + } catch (error) { + thrownError = error; + throw error; + } }; // Assert expect(act).toThrowError(DataSanitizationError); expect(act).toThrowError('Error parsing data'); + expect((thrownError as DataSanitizationError).details).toEqual({ + errorName: 'TypeError', + inputType: 'object', + }); + }); + + it('should not expose sensitive object input in parse error details', () => { + // Arrange + const input = { + password: 'abc"def', + username: 'mark', + }; + let thrownError: unknown; + + // Act + try { + sanitizeData(input); + } catch (error) { + thrownError = error; + } + + // Assert + expect(thrownError).toBeInstanceOf(DataSanitizationError); + expect((thrownError as Error).message).toEqual('Error parsing data'); + + const details = (thrownError as DataSanitizationError).details as Record< + string, + unknown + >; + const serializedDetails = JSON.stringify(details); + + expect(details).toEqual({ + errorName: 'SyntaxError', + inputType: 'object', + }); + expect(details).not.toHaveProperty('originalData'); + expect(details).not.toHaveProperty('error'); + expect(serializedDetails).not.toContain(input.password); + expect(serializedDetails).not.toContain(input.username); }); }); From 9b5f0a221d48ccc3217f182a0e29110162f94a5c Mon Sep 17 00:00:00 2001 From: Mark Jubenville Date: Sun, 17 May 2026 12:25:14 -0400 Subject: [PATCH 2/3] test: cover sanitization error detail types --- test/index-errors.test.ts | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/index-errors.test.ts b/test/index-errors.test.ts index f809887..71dc374 100644 --- a/test/index-errors.test.ts +++ b/test/index-errors.test.ts @@ -186,6 +186,55 @@ describe('DataSanitizationIndexAndErrors', () => { }); }); + it('should report null input type in wrapped error details', () => { + // Arrange + const input = null as unknown as Record; + const failingMatcher = (): RegExp => { + throw new Error('matcher failed'); + }; + let thrownError: unknown; + + // Act + try { + sanitizeData(input, { + customMatchers: [failingMatcher], + customPatterns: ['password'], + useDefaultMatchers: false, + useDefaultPatterns: false, + }); + } catch (error) { + thrownError = error; + } + + // Assert + expect(thrownError).toBeInstanceOf(DataSanitizationError); + expect((thrownError as DataSanitizationError).details).toEqual({ + errorName: 'Error', + inputType: 'null', + }); + }); + + it('should report array input type in wrapped error details', () => { + // Arrange + const input: unknown[] = []; + input.push(input); + let thrownError: unknown; + + // Act + try { + sanitizeData(input as unknown as Record); + } catch (error) { + thrownError = error; + } + + // Assert + expect(thrownError).toBeInstanceOf(DataSanitizationError); + expect((thrownError as DataSanitizationError).details).toEqual({ + errorName: 'TypeError', + inputType: 'array', + }); + }); + it('should not expose sensitive object input in parse error details', () => { // Arrange const input = { From 0aab408beeef736262e20720ed17328bf7ca0919 Mon Sep 17 00:00:00 2001 From: Mark Jubenville Date: Sun, 17 May 2026 12:49:43 -0400 Subject: [PATCH 3/3] fix: address some coderabbit pr comments --- test/index-errors.test.ts | 56 ++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/test/index-errors.test.ts b/test/index-errors.test.ts index 71dc374..a845f8b 100644 --- a/test/index-errors.test.ts +++ b/test/index-errors.test.ts @@ -195,18 +195,23 @@ describe('DataSanitizationIndexAndErrors', () => { let thrownError: unknown; // Act - try { - sanitizeData(input, { - customMatchers: [failingMatcher], - customPatterns: ['password'], - useDefaultMatchers: false, - useDefaultPatterns: false, - }); - } catch (error) { - thrownError = error; - } + const act = (): void => { + try { + sanitizeData(input, { + customMatchers: [failingMatcher], + customPatterns: ['password'], + useDefaultMatchers: false, + useDefaultPatterns: false, + }); + } catch (error) { + thrownError = error; + throw error; + } + }; // Assert + expect(act).toThrowError(DataSanitizationError); + expect(act).toThrowError('Error parsing data'); expect(thrownError).toBeInstanceOf(DataSanitizationError); expect((thrownError as DataSanitizationError).details).toEqual({ errorName: 'Error', @@ -221,13 +226,18 @@ describe('DataSanitizationIndexAndErrors', () => { let thrownError: unknown; // Act - try { - sanitizeData(input as unknown as Record); - } catch (error) { - thrownError = error; - } + const act = (): void => { + try { + sanitizeData(input as unknown as Record); + } catch (error) { + thrownError = error; + throw error; + } + }; // Assert + expect(act).toThrowError(DataSanitizationError); + expect(act).toThrowError('Error parsing data'); expect(thrownError).toBeInstanceOf(DataSanitizationError); expect((thrownError as DataSanitizationError).details).toEqual({ errorName: 'TypeError', @@ -244,15 +254,19 @@ describe('DataSanitizationIndexAndErrors', () => { let thrownError: unknown; // Act - try { - sanitizeData(input); - } catch (error) { - thrownError = error; - } + const act = (): void => { + try { + sanitizeData(input); + } catch (error) { + thrownError = error; + throw error; + } + }; // Assert + expect(act).toThrowError(DataSanitizationError); + expect(act).toThrowError('Error parsing data'); expect(thrownError).toBeInstanceOf(DataSanitizationError); - expect((thrownError as Error).message).toEqual('Error parsing data'); const details = (thrownError as DataSanitizationError).details as Record< string,