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..a845f8b 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,123 @@ 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 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 + 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', + inputType: 'null', + }); + }); + + it('should report array input type in wrapped error details', () => { + // Arrange + const input: unknown[] = []; + input.push(input); + let thrownError: unknown; + + // Act + 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', + inputType: 'array', + }); + }); + + it('should not expose sensitive object input in parse error details', () => { + // Arrange + const input = { + password: 'abc"def', + username: 'mark', + }; + let thrownError: unknown; + + // Act + 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); + + 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); }); });