Skip to content

Commit ef766ca

Browse files
feat(security): add input validation and sensitive data redaction
- Add centralized input-sanitizer module with zod schemas - Implement sensitive data redaction in logger (API keys, tokens, passwords) - Add search command input validation to prevent SQL LIKE injection - Add comprehensive security test suite (48 tests) Security improvements: - Path traversal prevention - Shell metacharacter validation - SQL identifier validation - Table name allowlist Closes: STA-187 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9748145 commit ef766ca

5 files changed

Lines changed: 939 additions & 13 deletions

File tree

src/cli/commands/search.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ import { Command } from 'commander';
77
import Database from 'better-sqlite3';
88
import { join } from 'path';
99
import { existsSync } from 'fs';
10+
import { z } from 'zod';
11+
12+
// Input validation schemas
13+
const SearchQuerySchema = z
14+
.string()
15+
.min(1, 'Search query is required')
16+
.max(500, 'Search query too long (max 500 characters)')
17+
.transform((val) => {
18+
// Escape SQL LIKE special characters to prevent injection
19+
return val.replace(/[%_\\]/g, '\\$&');
20+
});
21+
22+
const LimitSchema = z
23+
.string()
24+
.transform((val) => parseInt(val, 10))
25+
.pipe(z.number().int().min(1).max(100).default(20));
1026

1127
export function createSearchCommand(): Command {
1228
const search = new Command('search')
@@ -16,7 +32,7 @@ export function createSearchCommand(): Command {
1632
.option('-t, --tasks', 'Search only tasks')
1733
.option('-c, --context', 'Search only context')
1834
.option('-l, --limit <n>', 'Limit results', '20')
19-
.action(async (query, options) => {
35+
.action(async (rawQuery, options) => {
2036
const projectRoot = process.cwd();
2137
const dbPath = join(projectRoot, '.stackmemory', 'context.db');
2238

@@ -27,12 +43,28 @@ export function createSearchCommand(): Command {
2743
return;
2844
}
2945

46+
// Validate inputs
47+
let query: string;
48+
let limit: number;
49+
50+
try {
51+
query = SearchQuerySchema.parse(rawQuery);
52+
limit = LimitSchema.parse(options.limit);
53+
} catch (error) {
54+
if (error instanceof z.ZodError) {
55+
console.error('❌ Invalid input:', error.errors[0].message);
56+
} else {
57+
console.error('❌ Invalid input');
58+
}
59+
return;
60+
}
61+
3062
const db = new Database(dbPath);
31-
const limit = parseInt(options.limit);
3263
const searchTasks = !options.context || options.tasks;
3364
const searchContext = !options.tasks || options.context;
3465

35-
console.log(`\n🔍 Searching for "${query}"...\n`);
66+
// Display the original query (not the escaped one) for user
67+
console.log(`\n🔍 Searching for "${rawQuery}"...\n`);
3668

3769
let totalResults = 0;
3870

src/core/monitoring/logger.ts

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,84 @@
11
/**
22
* Structured logging utility for StackMemory CLI
3+
* Includes automatic sensitive data redaction for security
34
*/
45

56
import * as fs from 'fs';
67
import * as path from 'path';
7-
// Type-safe environment variable access
8-
function getEnv(key: string, defaultValue?: string): string {
9-
const value = process.env[key];
10-
if (value === undefined) {
11-
if (defaultValue !== undefined) return defaultValue;
12-
throw new Error(`Environment variable ${key} is required`);
8+
9+
/**
10+
* Sensitive data patterns that should be redacted from logs
11+
*/
12+
const SENSITIVE_PATTERNS = [
13+
/\b(api[_-]?key|apikey)\s*[:=]\s*['"]?[\w-]+['"]?/gi,
14+
/\b(secret|password|token|credential|auth)\s*[:=]\s*['"]?[\w-]+['"]?/gi,
15+
/\b(lin_api_[\w]+)/gi,
16+
/\b(lin_oauth_[\w]+)/gi,
17+
/\b(sk-[\w]+)/gi,
18+
/\b(npm_[\w]+)/gi,
19+
/\b(ghp_[\w]+)/gi,
20+
/\b(ghs_[\w]+)/gi,
21+
/Bearer\s+[\w.-]+/gi,
22+
/Basic\s+[\w=]+/gi,
23+
/postgres(ql)?:\/\/[^@\s]+:[^@\s]+@/gi,
24+
];
25+
26+
const SENSITIVE_FIELD_NAMES = [
27+
'password',
28+
'token',
29+
'apikey',
30+
'api_key',
31+
'secret',
32+
'credential',
33+
'authorization',
34+
'auth',
35+
'accesstoken',
36+
'access_token',
37+
'refreshtoken',
38+
'refresh_token',
39+
];
40+
41+
/**
42+
* Redact sensitive data from a string
43+
*/
44+
function redactString(input: string): string {
45+
let result = input;
46+
for (const pattern of SENSITIVE_PATTERNS) {
47+
pattern.lastIndex = 0;
48+
result = result.replace(pattern, '[REDACTED]');
1349
}
14-
return value;
50+
return result;
1551
}
1652

17-
function getOptionalEnv(key: string): string | undefined {
18-
return process.env[key];
53+
/**
54+
* Recursively sanitize an object for logging
55+
*/
56+
function sanitizeForLogging(obj: unknown): unknown {
57+
if (obj === null || obj === undefined) {
58+
return obj;
59+
}
60+
61+
if (typeof obj === 'string') {
62+
return redactString(obj);
63+
}
64+
65+
if (Array.isArray(obj)) {
66+
return obj.map(sanitizeForLogging);
67+
}
68+
69+
if (typeof obj === 'object') {
70+
const sanitized: Record<string, unknown> = {};
71+
for (const [key, value] of Object.entries(obj)) {
72+
if (SENSITIVE_FIELD_NAMES.some((sf) => key.toLowerCase().includes(sf))) {
73+
sanitized[key] = '[REDACTED]';
74+
} else {
75+
sanitized[key] = sanitizeForLogging(value);
76+
}
77+
}
78+
return sanitized;
79+
}
80+
81+
return obj;
1982
}
2083

2184
export enum LogLevel {
@@ -103,7 +166,15 @@ export class Logger {
103166
}
104167

105168
private writeLog(entry: LogEntry): void {
106-
const logLine = JSON.stringify(entry) + '\n';
169+
// Sanitize context and message to prevent logging sensitive data
170+
const sanitizedEntry: LogEntry = {
171+
...entry,
172+
message: redactString(entry.message),
173+
context: entry.context
174+
? (sanitizeForLogging(entry.context) as Record<string, unknown>)
175+
: undefined,
176+
};
177+
const logLine = JSON.stringify(sanitizedEntry) + '\n';
107178

108179
// Always write to file if configured
109180
if (this.logFile) {

0 commit comments

Comments
 (0)