Skip to content

Commit

Permalink
fix: fixed issues with masking circular references (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
roopak-chugh authored Nov 25, 2024
1 parent 33cb84b commit 8cd3a17
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 18 deletions.
38 changes: 20 additions & 18 deletions src/utils/mask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,29 @@ import { MaskableObject, MaskInput } from "../types";
* @param {MaskInput} obj - The object or array to mask. Can be undefined.
* @param {string[]} [fieldsToMask] - Array of field names to mask.
* Defaults to ['userName', 'userEmail']
* @param {WeakSet<object>} [visited] - Set of visited objects to avoid circular references.
* @returns {MaskInput} The object or array with specified fields masked
*/
const mask = (obj: MaskInput, fieldsToMask: string[] = []): MaskInput => {
/**
* Implementation:
* 1. If input is falsy or not an object, return as-is
* 2. For arrays, recursively mask each element
* 3. For objects:
* - If key matches fieldsToMask, replace value with "***"
* - If value is an object, recursively mask it
* - If value is a JSON string, parse and mask the parsed object
* - Otherwise keep value unchanged
* This ensures sensitive fields are masked at any nesting level,
* including within serialized JSON strings.
*/

const mask = (
obj: MaskInput,
fieldsToMask: string[] = [],
visited = new WeakSet<object>()
): MaskInput => {
const maskFieldsSet = new Set(fieldsToMask.concat(["userName", "userEmail"]));

if (!obj || typeof obj !== "object") {
return obj;
}

if (visited.has(obj)) {
// Avoid circular references
return undefined;
}

visited.add(obj);

if (Array.isArray(obj)) {
return obj.map((item) => mask(item, fieldsToMask)) as MaskInput;
return obj.map((item) => mask(item, fieldsToMask, visited)) as MaskInput;
}

return Object.fromEntries(
Expand All @@ -38,13 +37,16 @@ const mask = (obj: MaskInput, fieldsToMask: string[] = []): MaskInput => {
}

if (typeof value === "object" && value !== null) {
return [key, mask(value as MaskableObject, fieldsToMask)];
return [key, mask(value as MaskableObject, fieldsToMask, visited)];
}

if (typeof value === "string" && value.length > 0) {
try {
const parsedValue = JSON.parse(value) as MaskableObject;
return [key, JSON.stringify(mask(parsedValue, fieldsToMask))];
return [
key,
JSON.stringify(mask(parsedValue, fieldsToMask, visited)),
];
} catch (err) {
// Not a valid JSON string
}
Expand Down Expand Up @@ -90,7 +92,7 @@ const getMaskedValue = (value: unknown) => {
// Rejoin with original delimiters
return value
.split(/(@|\s|\.)/)
.map((part, index) => {
.map((part) => {
if (part === "@" || part === " " || part === ".") {
return part;
}
Expand Down
12 changes: 12 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,15 @@ loggerWithCustomMaskFields.info(
"This is a sample logger with masked metadata",
metadataWithSensitiveData
);

const foo = {
foo: "Foo",
bar: {
bar: "Bar",
userName: "John Doe",
},
};

foo.bar.baz = foo; // Circular reference!

logger.info("This is a sample logger with circular reference", foo);
19 changes: 19 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,22 @@ loggerWithCustomMaskFields.info(
"This is a sample logger with masked metadata",
metadataWithSensitiveData
);

const foo: {
foo: string;
bar: {
bar: string;
userName: string;
baz?: any;
};
} = {
foo: "Foo",
bar: {
bar: "Bar",
userName: "John Doe",
},
};

foo.bar.baz = foo; // Circular reference!

logger.info("This is a sample logger with circular reference", foo);

0 comments on commit 8cd3a17

Please sign in to comment.