Skip to content

Commit 009e628

Browse files
committed
fix(config): add path validation to prevent directory traversal (Issue #12)
Added comprehensive validation for configuration path keys to prevent injection attacks. Changes: - Added validateKeyString() method to check original key before splitting - Enhanced validatePath() to check individual path segments - Validates against directory traversal: "..", "../", "..\\" - Validates against path separators: "/" and "\\" - Validates against null bytes: "\0" - Validates against leading dots: "." prefix - Validates against empty segments: "auth..apiKey" Security Impact: - Closes vulnerability allowing malicious config keys - Prevents directory traversal attacks via config paths - Prevents injection attacks via null bytes - Prevents access to hidden files via leading dots Testing: - Added 7 comprehensive security tests covering all attack vectors - All 1461 tests pass (up from 1454) - Tests verify both rejection of malicious paths and acceptance of valid paths Technical Details: - Two-stage validation: original string + split segments - validateKeyString() checks patterns lost in split (e.g., ".." in "../auth") - validatePath() checks individual segments after split - Proper error messages for each validation failure Example Attack Prevented: configService.set('../../../etc/passwd', 'data') Location: src/storage/config.ts:72,389-406
1 parent f56618d commit 009e628

3 files changed

Lines changed: 121 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8585
- Location: `src/services/watch.ts:172-186` (timer callback cleanup)
8686

8787
### Security
88+
- **Config Path Validation** - Prevents directory traversal and injection attacks (Issue #12)
89+
- Added comprehensive validation for configuration path keys
90+
- Rejects directory traversal patterns: "..", "../", "..\\"
91+
- Rejects path separators: "/" and "\\" in path segments
92+
- Rejects null bytes: "\0" (common injection technique)
93+
- Rejects leading dots: paths starting with "." (hidden files/directories)
94+
- Rejects empty path segments: "auth..apiKey" creates empty segment
95+
- Validation happens in two stages: original string + split segments
96+
- Added 7 comprehensive security tests covering all attack vectors
97+
- **Impact**: Closes security vulnerability allowing malicious config keys
98+
- **Example Attack Prevented**: `configService.set('../../../etc/passwd', 'data')`
99+
- **Technical Detail**: validateKeyString() checks original key, validatePath() checks segments
100+
- Location: `src/storage/config.ts:72,389-406` (validation methods)
101+
88102
- **Symlink Path Validation** - Prevents directory traversal attacks (Issue #6)
89103
- Rejects symlinks at translate() entry point before processing files
90104
- Uses `fs.lstatSync()` to detect symlinks (doesn't follow symlinks)

src/storage/config.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,12 @@ export class ConfigService {
6565

6666
/**
6767
* Set a configuration value by path
68+
* Issue #12: Security validation on original key before splitting
6869
*/
6970
set(key: string, value: unknown): void {
71+
// Issue #12: Validate original key string before splitting
72+
this.validateKeyString(key);
73+
7074
const keys = key.split('.');
7175
this.validatePath(keys, value);
7276

@@ -267,12 +271,46 @@ export class ConfigService {
267271

268272
/**
269273
* Validate configuration path and value
274+
* Issue #12: Security validation to prevent directory traversal and injection attacks
270275
*/
271276
private validatePath(keys: string[], value: unknown): void {
272277
if (keys.length === 0) {
273278
throw new Error('Invalid path: empty');
274279
}
275280

281+
// Issue #12: Security validation - check each path segment for malicious patterns
282+
for (const key of keys) {
283+
// Check for null bytes first (common injection technique)
284+
if (key.includes('\0')) {
285+
throw new Error('Invalid path: contains null byte');
286+
}
287+
288+
// Check for path separators (Unix and Windows)
289+
if (key.includes('/') || key.includes('\\')) {
290+
throw new Error('Invalid path: contains path separator');
291+
}
292+
293+
// Check for directory traversal patterns
294+
// Note: ".." in a path like "../auth" becomes ["", "", "auth"] after split
295+
// So we check for ".." OR check if key is empty AND previous key was also empty
296+
if (key === '..' || key.includes('..')) {
297+
throw new Error('Invalid path: contains directory traversal');
298+
}
299+
300+
// Check for leading dots (handles paths starting with "." like ".auth")
301+
// Note: ".auth" becomes ["", "auth"] after split, so first element is empty
302+
// We check if segment is exactly "." or starts with "." (for segments like ".hidden")
303+
if (key === '.' || (key.startsWith('.') && key.length > 0)) {
304+
throw new Error('Invalid path: segment starts with dot');
305+
}
306+
307+
// Check for empty segments last (catch-all for other cases)
308+
// This catches cases like "auth..apiKey" which splits to ["auth", "", "apiKey"]
309+
if (key === '') {
310+
throw new Error('Invalid path: empty segment');
311+
}
312+
}
313+
276314
const path = keys.join('.');
277315

278316
// Validate specific paths
@@ -343,4 +381,27 @@ export class ConfigService {
343381
throw new Error('Invalid output format');
344382
}
345383
}
384+
385+
/**
386+
* Validate key string for security issues (Issue #12)
387+
* Checks original key before splitting to catch patterns that would be lost in split
388+
*/
389+
private validateKeyString(key: string): void {
390+
// Check for directory traversal in original string
391+
// This catches patterns like "../auth" or "auth/../key"
392+
if (key.includes('../') || key.includes('..\\') || key.startsWith('..')) {
393+
throw new Error('Invalid path: contains directory traversal');
394+
}
395+
396+
// Check for leading dot (hidden files/directories)
397+
// This catches ".auth.apiKey" before it's split into ["", "auth", "apiKey"]
398+
if (key.startsWith('.')) {
399+
throw new Error('Invalid path: segment starts with dot');
400+
}
401+
402+
// Check for consecutive dots (empty segments)
403+
if (key.includes('..')) {
404+
throw new Error('Invalid path: contains directory traversal');
405+
}
406+
}
346407
}

tests/unit/config-service.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,50 @@ describe('ConfigService', () => {
299299
expect(Array.isArray(langs)).toBe(true);
300300
});
301301
});
302+
303+
describe('path security validation (Issue #12)', () => {
304+
it('should reject paths with directory traversal (..)', () => {
305+
expect(() => {
306+
configService.set('../auth.apiKey', 'malicious');
307+
}).toThrow('Invalid path: contains directory traversal');
308+
});
309+
310+
it('should reject paths with forward slashes', () => {
311+
expect(() => {
312+
configService.set('auth/apiKey', 'malicious');
313+
}).toThrow('Invalid path: contains path separator');
314+
});
315+
316+
it('should reject paths with backslashes', () => {
317+
expect(() => {
318+
configService.set('auth\\apiKey', 'malicious');
319+
}).toThrow('Invalid path: contains path separator');
320+
});
321+
322+
it('should reject paths with null bytes', () => {
323+
expect(() => {
324+
configService.set('auth\0apiKey', 'malicious');
325+
}).toThrow('Invalid path: contains null byte');
326+
});
327+
328+
it('should reject empty path segments (consecutive dots)', () => {
329+
// Note: ".." is caught by directory traversal check (more serious security issue)
330+
expect(() => {
331+
configService.set('auth..apiKey', 'malicious');
332+
}).toThrow('Invalid path: contains directory traversal');
333+
});
334+
335+
it('should reject paths with leading dots', () => {
336+
expect(() => {
337+
configService.set('.auth.apiKey', 'malicious');
338+
}).toThrow('Invalid path: segment starts with dot');
339+
});
340+
341+
it('should accept valid paths with dots in segments', () => {
342+
// This should be allowed: segments themselves are just "auth" and "apiKey"
343+
expect(() => {
344+
configService.set('auth.apiKey', 'valid-key');
345+
}).not.toThrow();
346+
});
347+
});
302348
});

0 commit comments

Comments
 (0)