- Location
- scripts/src/core/storage.ts:58
- Finding
- Exported Storage Utilities Permit Filesystem Path Traversal<![CDATA[
## Vulnerability Details
**File Location**: `scripts/src/core/storage.ts:58-126`
**Additional Export Location**: `scripts/src/index.ts:22`
**Vulnerability Type**: Path traversal and unrestricted file access
**Risk Level**: Medium
### Vulnerable Code
```typescript
export function saveResult<T>(
data: T,
category: string,
operation: string,
extraInfo?: string
): string {
const settings = getSettings();
const categoryDir = join(settings.resultsDir, category);
// Ensure category directory exists
if (!existsSync(categoryDir)) {
mkdirSync(categoryDir, { recursive: true });
}
// Build filename
const timestamp = getTimestamp();
const sanitizedOperation = sanitizeFilename(operation);
const sanitizedExtra = extraInfo ? `__${sanitizeFilename(extraInfo)}` : '';
const filename = `${timestamp}__${sanitizedOperation}${sanitizedExtra}.json`;
const filepath = join(categoryDir, filename);
// Build wrapped result
const result: SavedResult<T> = {
metadata: {
savedAt: new Date().toISOString(),
category,
operation,
propertyId: settings.propertyId,
...(extraInfo && { extraInfo }),
},
data,
};
// Write to file
writeFileSync(filepath, JSON.stringify(result, null, 2), 'utf-8');
return filepath;
}
export function loadResult<T = unknown>(filepath: string): SavedResult<T> | null {
if (!existsSync(filepath)) {
return null;
}
try {
const content = readFileSync(filepath, 'utf-8');
return JSON.parse(content) as SavedResult<T>;
} catch {
return null;
}
}
export function listResults(category: string, limit?: number): string[] {
const settings = getSettings();
const categoryDir = join(settings.resultsDir, category);
if (!existsSync(categoryDir)) {
return [];
}
const files = readdirSync(categoryDir)
.filter(f => f.endsWith('.json'))
.map(f => join(categoryDir, f))
.sort((a, b) => {
const nameA = a.split('/').pop() || '';
co
...[truncated 3412 chars]
- Remediation
- <![CDATA[
## Remediation Suggestions
1. Resolve the result root to a canonical absolute path:
```typescript
import { resolve, sep } from 'path';
const resultsRoot = resolve(getSettings().resultsDir);
```
2. Allow only predefined categories rather than arbitrary directory names:
```typescript
const ALLOWED_CATEGORIES = new Set([
'reports',
'realtime',
'searchconsole',
'indexing',
'metadata',
'summaries',
]);
function validateCategory(category: string): string {
if (!ALLOWED_CATEGORIES.has(category)) {
throw new Error('Invalid result category');
}
return category;
}
```
3. Enforce containment after resolving every path:
```typescript
function resolveWithinResults(relativePath: string): string {
const target = resolve(resultsRoot, relativePath);
if (target !== resultsRoot && !target.startsWith(resultsRoot + sep)) {
throw new Error('Path escapes the results directory');
}
return target;
}
```
4. Reject absolute paths, null bytes, and traversal components before filesystem access.
5. Replace `loadResult(filepath)` with an API accepting a validated category and generated result filename:
```typescript
loadResult(category, filename);
```
6. Validate that filenames match the expected timestamped result format and contain no path separators.
7. Apply containment checks to `saveResult()`, `loadResult()`, `listResults()`, and `getLatestResult()`.
8. Regenerate or patch the compiled files under `scripts/dist/` so runtime behavior matches the corrected TypeScript source.
9. Add tests covering absolute paths, `../` traversal, nested traversal, platform-specific separators, symbolic links, and valid category access. For stronger protection against symlink-based escapes, verify the canonical path with `realpath()` before reading or writing.
]]>