Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .bumpy/fix-sensitive-number-redaction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
varlock: patch
---

Register numeric @sensitive values for leak scanning and redaction
9 changes: 7 additions & 2 deletions packages/varlock/src/lib/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,13 @@ export function getItemSummary(item: ConfigItem) {
]));

let valAsStr = formattedValue(item.resolvedValue, false);
if (isSensitive && item.resolvedValue && _.isString(item.resolvedValue)) {
valAsStr = redactString(item.resolvedValue)!;
if (
isSensitive
&& item.resolvedValue !== undefined
&& item.resolvedValue !== null
&& (_.isString(item.resolvedValue) || (_.isNumber(item.resolvedValue) && Number.isFinite(item.resolvedValue)))
) {
valAsStr = redactString(String(item.resolvedValue))!;
}

// build inline indicators to append after the value
Expand Down
11 changes: 9 additions & 2 deletions packages/varlock/src/runtime/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,16 @@ function getRedactionState(): RedactionState {

/** collect every redactable string within a (possibly composite) sensitive value -
* for arrays/objects each string element registers individually, so leaking a single
* element (not just the whole serialized value) is still caught */
* element (not just the whole serialized value) is still caught.
* Finite numbers are included (e.g. `@type=number @sensitive` PINs) because they are
* injected into process.env as strings and must still be scanned/redacted.
* Booleans are intentionally skipped: registering "true"/"false" would false-positive
* almost every response body. */
function collectSensitiveStrings(value: any, collected: Array<string> = []): Array<string> {
if (isString(value) && value) {
collected.push(value as string);
} else if (typeof value === 'number' && Number.isFinite(value)) {
collected.push(String(value));
} else if (Array.isArray(value)) {
for (const el of value) collectSensitiveStrings(el, collected);
} else if (value && typeof value === 'object') {
Expand All @@ -58,7 +64,8 @@ export function resetRedactionMap(graph: SerializedEnvGraph) {
state.sensitiveSecretsMap = {};
for (const itemKey in graph.config) {
const item = graph.config[itemKey];
if (!item.isSensitive || !item.value) continue;
// Use nullish check so sensitive numeric `0` is still registered (!value would skip it)
if (!item.isSensitive || item.value === undefined || item.value === null) continue;
const sensitiveStrings = collectSensitiveStrings(item.value);
// the flat serialized form also registers (e.g. a JSON-encoded element may not
// match its raw form once escaped)
Expand Down
34 changes: 34 additions & 0 deletions packages/varlock/src/runtime/test/scan-for-leaks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,40 @@ describe('scanForLeaks', () => {
});
});

describe('scanForLeaks with numeric sensitive values', () => {
it('detects a leaked numeric secret (e.g. @type=number @sensitive)', () => {
resetRedactionMap({
config: {
PIN: { isSensitive: true, value: 48291736 },
},
} as unknown as SerializedEnvGraph);

expect(() => scanForLeaks('payload 48291736 end'))
.toThrow(/DETECTED LEAKED SENSITIVE CONFIG/);
expect(redactSensitiveConfig('pin=48291736')).not.toContain('48291736');
});

it('registers sensitive numeric zero (must not skip via falsy check)', () => {
resetRedactionMap({
config: {
ZERO_TOKEN: { isSensitive: true, value: 0 },
},
} as unknown as SerializedEnvGraph);

expect(() => scanForLeaks('code=0;')).toThrow(/DETECTED LEAKED SENSITIVE CONFIG/);
});

it('detects numeric secrets nested in arrays/objects', () => {
resetRedactionMap({
config: {
CREDS: { isSensitive: true, value: { pin: 991122, label: 'x' } },
},
} as unknown as SerializedEnvGraph);

expect(() => scanForLeaks('pin=991122')).toThrow(/DETECTED LEAKED SENSITIVE CONFIG/);
});
});

describe('scanForLeaks with per-item preventLeaks opt-out', () => {
const LEAKY_VALUE = 'allowed-to-leave-67890';
const NORMAL_VALUE = 'still-protected-12345';
Expand Down
Loading