fix: prevent sensitive data logging (#70)
* feat: Add sensitive data masking to packet logging by introducing a sanitization function and a list of sensitive keys. * test: mock console methods in logger tests and restore after each test * refactor: remove redundant sensitive data masking functions and integrate safeStringifyPacket utility for improved logging * fix: run format:write * test: enhance logger tests with error handling for directory setup and cleanup * refactor: optimize sensitive data masking logic by checking for exact matches in sensitive keys * refactor: enhance sensitive data masking to handle circular references and improve sanitization logic * refactor: update sensitive data masking comments and test descriptions to improve clarity and consistency * test: enhance logger tests with dynamic log directory creation and cleanup after tests * test: add unit tests for handling malformed JSON strings in sensitive data masking
This commit is contained in:
1 parent
56137092be
commit
5155e37fd1
5 files changed
+238
-22
No files matched your search
+4
-4
@@ -182,11 +182,11 @@
|
|||||||
|
|
||||||
从 [Releases](https://github.com/Draculabo/AntigravityManager/releases) 页面下载适合你平台的最新版本。
|
从 [Releases](https://github.com/Draculabo/AntigravityManager/releases) 页面下载适合你平台的最新版本。
|
||||||
|
|
||||||
| 平台 | 下载链接 |
|
| 平台 | 下载链接 |
|
||||||
| ------- | ------------------------------------------------------------------------------ |
|
| ------------------- | ------------------------------------------------------------------------------ |
|
||||||
| Windows (x64/ARM64) | [.exe 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
| Windows (x64/ARM64) | [.exe 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
||||||
| macOS | [.dmg 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
| macOS | [.dmg 安装包](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
||||||
| Linux | [.deb / .rpm](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
| Linux | [.deb / .rpm](https://github.com/Draculabo/AntigravityManager/releases/latest) |
|
||||||
|
|
||||||
### 从源码构建
|
### 从源码构建
|
||||||
|
|
||||||
|
|||||||
+1
-16
@@ -28,25 +28,10 @@ import { rpcHandler } from './ipc/handler';
|
|||||||
import { ConfigManager } from './ipc/config/manager';
|
import { ConfigManager } from './ipc/config/manager';
|
||||||
import { AppConfig } from './types/config';
|
import { AppConfig } from './types/config';
|
||||||
import { isAutoStartLaunch, syncAutoStart } from './utils/autoStart';
|
import { isAutoStartLaunch, syncAutoStart } from './utils/autoStart';
|
||||||
|
import { safeStringifyPacket } from './utils/sensitiveDataMasking';
|
||||||
|
|
||||||
const packetLogPath = path.join(app.getPath('userData'), 'orpc_packets.log');
|
const packetLogPath = path.join(app.getPath('userData'), 'orpc_packets.log');
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely stringify an object, handling circular references
|
|
||||||
*/
|
|
||||||
function safeStringifyPacket(obj: unknown): string {
|
|
||||||
const seen = new WeakSet();
|
|
||||||
return JSON.stringify(obj, (key, value) => {
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
if (seen.has(value)) {
|
|
||||||
return '[Circular]';
|
|
||||||
}
|
|
||||||
seen.add(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function logPacket(data: any) {
|
function logPacket(data: any) {
|
||||||
try {
|
try {
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -42,8 +42,38 @@ describe('Logger Utilities', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
if (!fs.existsSync(testLogDir)) {
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(testLogDir)) {
|
||||||
|
fs.rmSync(testLogDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
fs.mkdirSync(testLogDir, { recursive: true });
|
fs.mkdirSync(testLogDir, { recursive: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('beforeEach: setup testLogDir failed', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(testLogDir)) {
|
||||||
|
fs.rmSync(testLogDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('afterEach: cleanup testLogDir failed', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(testLogDir)) {
|
||||||
|
fs.rmSync(testLogDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('afterAll: cleanup testLogDir failed', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { sanitizeObject, safeStringifyPacket } from '@/utils/sensitiveDataMasking';
|
||||||
|
|
||||||
|
describe('sensitive data masking', () => {
|
||||||
|
describe('sanitizeObject', () => {
|
||||||
|
it('masks password', () => {
|
||||||
|
expect(sanitizeObject({ password: 'secret123' })).toEqual({ password: '[REDACTED]' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('masks token and variants (case-insensitive)', () => {
|
||||||
|
expect(sanitizeObject({ token: 'abc' })).toEqual({ token: '[REDACTED]' });
|
||||||
|
expect(sanitizeObject({ Authorization: 'Bearer xyz' })).toEqual({
|
||||||
|
Authorization: '[REDACTED]',
|
||||||
|
});
|
||||||
|
expect(sanitizeObject({ api_key: 'key123' })).toEqual({ api_key: '[REDACTED]' });
|
||||||
|
expect(sanitizeObject({ refresh_token: 'rt' })).toEqual({ refresh_token: '[REDACTED]' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('masks nested keys', () => {
|
||||||
|
expect(
|
||||||
|
sanitizeObject({
|
||||||
|
user: { name: 'Alice', password: 'pwd', nested: { token: 't' } },
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
user: { name: 'Alice', password: '[REDACTED]', nested: { token: '[REDACTED]' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mask non-sensitive keys', () => {
|
||||||
|
expect(sanitizeObject({ name: 'Alice', id: 1 })).toEqual({ name: 'Alice', id: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles null and undefined', () => {
|
||||||
|
expect(sanitizeObject(null)).toBe(null);
|
||||||
|
expect(sanitizeObject(undefined)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles arrays recursively', () => {
|
||||||
|
expect(sanitizeObject([{ password: 'x' }, { name: 'y' }])).toEqual([
|
||||||
|
{ password: '[REDACTED]' },
|
||||||
|
{ name: 'y' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles JSON string with sensitive fields', () => {
|
||||||
|
const json = JSON.stringify({ password: 'hidden' });
|
||||||
|
expect(sanitizeObject(json)).toBe(JSON.stringify({ password: '[REDACTED]' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves non-JSON string unchanged', () => {
|
||||||
|
expect(sanitizeObject('plain text')).toBe('plain text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves malformed JSON string unchanged (no throw)', () => {
|
||||||
|
expect(sanitizeObject('{"broken":')).toBe('{"broken":');
|
||||||
|
expect(sanitizeObject('not valid json ]')).toBe('not valid json ]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles circular references without infinite loops', () => {
|
||||||
|
const circular: Record<string, unknown> = { name: 'a', password: 'secret' };
|
||||||
|
circular.self = circular;
|
||||||
|
expect(sanitizeObject(circular)).toEqual({
|
||||||
|
name: 'a',
|
||||||
|
password: '[REDACTED]',
|
||||||
|
self: '[Circular]',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('masks session_id, cookie, client_secret, otp, pin', () => {
|
||||||
|
expect(
|
||||||
|
sanitizeObject({
|
||||||
|
session_id: 'sess',
|
||||||
|
cookie: 'c',
|
||||||
|
client_secret: 'cs',
|
||||||
|
otp: '1234',
|
||||||
|
pin: '0000',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
session_id: '[REDACTED]',
|
||||||
|
cookie: '[REDACTED]',
|
||||||
|
client_secret: '[REDACTED]',
|
||||||
|
otp: '[REDACTED]',
|
||||||
|
pin: '[REDACTED]',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('safeStringifyPacket', () => {
|
||||||
|
it('stringifies with sensitive fields masked', () => {
|
||||||
|
const out = safeStringifyPacket({ user: 'a', password: 'p' });
|
||||||
|
expect(out).toContain('"user":"a"');
|
||||||
|
expect(out).toContain('"password":"[REDACTED]"');
|
||||||
|
expect(() => JSON.parse(out)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Keys to mask in logs to avoid exposing sensitive data.
|
||||||
|
*/
|
||||||
|
const SENSITIVE_KEYS = [
|
||||||
|
'password',
|
||||||
|
'token',
|
||||||
|
'apikey',
|
||||||
|
'api_key',
|
||||||
|
'secret',
|
||||||
|
'authorization',
|
||||||
|
'credentials',
|
||||||
|
'accesstoken',
|
||||||
|
'access_token',
|
||||||
|
'refreshtoken',
|
||||||
|
'refresh_token',
|
||||||
|
'bearertoken',
|
||||||
|
'bearer_token',
|
||||||
|
'sessionid',
|
||||||
|
'session_id',
|
||||||
|
'cookie',
|
||||||
|
'private_key',
|
||||||
|
'privatekey',
|
||||||
|
'client_secret',
|
||||||
|
'clientsecret',
|
||||||
|
'auth',
|
||||||
|
'authcode',
|
||||||
|
'auth_code',
|
||||||
|
'code',
|
||||||
|
'otp',
|
||||||
|
'pin',
|
||||||
|
'verificationcode',
|
||||||
|
'verification_code',
|
||||||
|
];
|
||||||
|
|
||||||
|
const CIRCULAR_PLACEHOLDER = '[Circular]';
|
||||||
|
|
||||||
|
function sanitizeWithSeen(obj: unknown, seen: WeakSet<object>): unknown {
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(obj);
|
||||||
|
if (typeof parsed === 'object' && parsed !== null) {
|
||||||
|
return JSON.stringify(sanitizeWithSeen(parsed, seen));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not JSON, return unchanged
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
if (seen.has(obj)) {
|
||||||
|
return CIRCULAR_PLACEHOLDER;
|
||||||
|
}
|
||||||
|
seen.add(obj);
|
||||||
|
return obj.map((item) => sanitizeWithSeen(item, seen));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
if (seen.has(obj)) {
|
||||||
|
return CIRCULAR_PLACEHOLDER;
|
||||||
|
}
|
||||||
|
seen.add(obj);
|
||||||
|
const sanitized: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
const lowerKey = key.toLowerCase();
|
||||||
|
if (SENSITIVE_KEYS.includes(lowerKey)) {
|
||||||
|
sanitized[key] = '[REDACTED]';
|
||||||
|
} else {
|
||||||
|
sanitized[key] = sanitizeWithSeen(value, seen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively sanitizes an object by masking sensitive field values.
|
||||||
|
* Handles circular references by replacing them with '[Circular]'.
|
||||||
|
*/
|
||||||
|
export function sanitizeObject(obj: unknown): unknown {
|
||||||
|
return sanitizeWithSeen(obj, new WeakSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely stringifies an object, handling circular references and masking sensitive data.
|
||||||
|
*/
|
||||||
|
export function safeStringifyPacket(obj: unknown): string {
|
||||||
|
const sanitized = sanitizeObject(obj);
|
||||||
|
const seen = new WeakSet();
|
||||||
|
return JSON.stringify(sanitized, (key, value) => {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
if (seen.has(value)) {
|
||||||
|
return '[Circular]';
|
||||||
|
}
|
||||||
|
seen.add(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user