Skip to content
Merged
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
42 changes: 42 additions & 0 deletions frontend/LOGGING_SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Logging Security - Issue #232

## Implementation

Added secure logging utility with automatic PII and secret redaction.

### Redacted Data

**Secrets:**
- Tokens (auth, bearer, API keys)
- Private keys
- Passwords
- Seeds/mnemonics

**PII:**
- Email addresses (shows first 2 chars + domain)
- Wallet addresses (shows first 4 + last 4 chars)

### Usage

```typescript
import { logger } from '@/lib/logger';

// Safe logging - automatically redacts sensitive data
logger.info('User connected', {
address: 'GBRP...',
email: 'user@example.com'
});

logger.error('Auth failed', { token: 'abc123' });
```

### Files Modified

- `src/lib/logger.ts` - Secure logger utility
- `src/lib/__tests__/logger.test.ts` - Redaction tests

### Testing

Run tests: `npm test logger.test.ts`

All sensitive fields are redacted before logging.
58 changes: 58 additions & 0 deletions frontend/src/lib/__tests__/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Tests for secure logger redaction
*/

import { logger } from '../logger';

describe('Logger Redaction', () => {
let consoleLog: jest.SpyInstance;
let consoleError: jest.SpyInstance;

beforeEach(() => {
consoleLog = jest.spyOn(console, 'log').mockImplementation();
consoleError = jest.spyOn(console, 'error').mockImplementation();
});

afterEach(() => {
consoleLog.mockRestore();
consoleError.mockRestore();
});

it('redacts tokens', () => {
logger.info('Auth', { token: 'secret_token_12345678' });
expect(consoleLog).toHaveBeenCalledWith('[INFO] Auth', expect.objectContaining({
token: 'secr...5678'
}));
});

it('redacts wallet addresses', () => {
logger.info('Wallet', { address: 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H' });
expect(consoleLog).toHaveBeenCalledWith('[INFO] Wallet', expect.objectContaining({
address: 'GBRP...OX2H'
}));
});

it('redacts emails', () => {
logger.info('User', { email: 'user@example.com' });
expect(consoleLog).toHaveBeenCalledWith('[INFO] User', expect.objectContaining({
email: 'us***@example.com'
}));
});

it('redacts private keys', () => {
logger.error('Key error', { privateKey: 'SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' });
expect(consoleError).toHaveBeenCalledWith('[ERROR] Key error', expect.objectContaining({
privateKey: expect.stringContaining('...')
}));
});

it('redacts nested objects', () => {
logger.info('Nested', { user: { email: 'test@test.com', token: 'abc123xyz' } });
expect(consoleLog).toHaveBeenCalledWith('[INFO] Nested', expect.objectContaining({
user: expect.objectContaining({
email: expect.stringContaining('***'),
token: expect.stringContaining('...')
})
}));
});
});
61 changes: 61 additions & 0 deletions frontend/src/lib/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Secure logger with PII and secret redaction
*/

const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'authorization', 'privateKey', 'seed', 'mnemonic'];
const ADDRESS_PATTERN = /G[A-Z0-9]{55}/g;
const EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;

function redactValue(key: string, value: unknown): unknown {
if (typeof value === 'string') {
// Redact auth tokens and keys
if (SENSITIVE_KEYS.some(k => key.toLowerCase().includes(k))) {
return value.length > 8 ? `${value.slice(0, 4)}...${value.slice(-4)}` : '[REDACTED]';
}
// Redact wallet addresses
if (key.toLowerCase().includes('address') || key.toLowerCase().includes('wallet')) {
return value.replace(ADDRESS_PATTERN, (addr) => `${addr.slice(0, 4)}...${addr.slice(-4)}`);
}
// Redact emails
if (key.toLowerCase().includes('email')) {
return value.replace(EMAIL_PATTERN, (email) => {
const [local, domain] = email.split('@');
return `${local.slice(0, 2)}***@${domain}`;
});
}
}
return value;
}

function redactObject(obj: unknown): unknown {
if (!obj || typeof obj !== 'object') return obj;

if (Array.isArray(obj)) {
return obj.map(item => redactObject(item));
}

const redacted: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
redacted[key] = typeof value === 'object' ? redactObject(value) : redactValue(key, value);
}
return redacted;
}

export const logger = {
info: (message: string, data?: unknown) => {
if (process.env.NODE_ENV !== 'production') {
console.log(`[INFO] ${message}`, data ? redactObject(data) : '');
}
},

error: (message: string, error?: unknown) => {
const redacted = error instanceof Error
? { message: error.message, name: error.name }
: redactObject(error);
console.error(`[ERROR] ${message}`, redacted);
},

warn: (message: string, data?: unknown) => {
console.warn(`[WARN] ${message}`, data ? redactObject(data) : '');
}
};