Skip to content
Closed
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
3 changes: 3 additions & 0 deletions packages/scanner/src/detectors/injection.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import type { Finding } from '../types.js'
import { validatePath } from '../utils.js'

/**
* Load declared packages from package.json
*/
function loadPackageJsonDeps(targetDir: string): Set<string> {
validatePath(targetDir)
try {
const pkg = JSON.parse(readFileSync(resolve(targetDir, 'package.json'), 'utf-8')) as any
const declared = new Set<string>()
Expand All @@ -31,6 +33,7 @@ export function detectInjection(
lockfilePackages: Array<{ name: string; version?: string }>,
targetDir: string
): Finding[] {
validatePath(targetDir)
const findings: Finding[] = []
const declared = loadPackageJsonDeps(targetDir)

Expand Down
2 changes: 2 additions & 0 deletions packages/scanner/src/parsers/npm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import type { LockfileEntry } from '../types.js'
import { validatePath } from '../utils.js'

interface NpmLockfile {
lockfileVersion?: number
Expand All @@ -27,6 +28,7 @@ export interface ParsedPackage {

/** Parse package-lock.json (v2/v3) or package-lock.v3.json */
export function parseNpmLockfile(targetDir: string): ParsedPackage[] {
validatePath(targetDir)
const paths = [
join(targetDir, 'package-lock.json'),
join(targetDir, 'package-lock.v3.json'),
Expand Down
2 changes: 2 additions & 0 deletions packages/scanner/src/parsers/yarn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import type { LockfileEntry } from '../types.js'
import { validatePath } from '../utils.js'

export interface ParsedPackage extends LockfileEntry {
name: string
Expand Down Expand Up @@ -88,6 +89,7 @@ function parseYarnV2(content: string): ParsedPackage[] {
* Main entrance for Yarn lockfile parsing
*/
export function parseYarnLockfile(targetDir: string): ParsedPackage[] {
validatePath(targetDir)
const lockPath = join(targetDir, 'yarn.lock')
let content: string
try {
Expand Down
5 changes: 5 additions & 0 deletions packages/scanner/src/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { basename, resolve } from 'node:path'

import { parseYarnLockfile } from './parsers/yarn.js'
import type { Finding, ScanResult } from './types.js'
import { validatePath } from './utils.js'

// ---------------------------------------------------------------------------
// Lockfile discovery
Expand All @@ -18,6 +19,7 @@ const LOCKFILE_NAMES = [
]

function findLockfiles(target: string): string[] {
validatePath(target)
const found: string[] = []
for (const name of LOCKFILE_NAMES) {
const path = resolve(target, name)
Expand Down Expand Up @@ -115,6 +117,7 @@ function detectInjection(
packages: Array<{ name: string; version: string }>,
target: string
): Finding[] {
validatePath(target)
let pkgJson: any = {}
try {
pkgJson = JSON.parse(readFileSync(resolve(target, 'package.json'), 'utf8'))
Expand Down Expand Up @@ -157,6 +160,7 @@ function detectInjection(
// ---------------------------------------------------------------------------

export async function scan(target: string): Promise<ScanResult> {
validatePath(target)
const start = Date.now()
const findings: Finding[] = []

Expand Down Expand Up @@ -190,6 +194,7 @@ export async function scan(target: string): Promise<ScanResult> {
* CLI entry point
*/
export async function cli(target: string): Promise<number> {
validatePath(target)
console.log(`[worms-scan] Scanning: ${target}`)
const result = await scan(target)

Expand Down
18 changes: 18 additions & 0 deletions packages/scanner/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Validates a path to prevent path traversal and other injection attacks.
* @param path - The path to validate
* @throws {Error} If the path is invalid
*/
export function validatePath(path: string): void {
if (typeof path !== 'string') {
throw new Error('Invalid path: must be a string');
}

if (path.includes('\0')) {
throw new Error('Invalid path: null byte detected');
}

if (!path || path.trim() === '') {
throw new Error('Invalid path: path cannot be empty');
}
}
30 changes: 30 additions & 0 deletions packages/scanner/tests/security.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'bun:test';
import { validatePath } from '../src/utils';
import { scan } from '../src/scan';

describe('Path Validation', () => {
it('should throw error for null bytes', () => {
expect(() => validatePath('/etc/passwd\0')).toThrow('Invalid path: null byte detected');
});

it('should throw error for empty paths', () => {
expect(() => validatePath('')).toThrow('Invalid path: path cannot be empty');
expect(() => validatePath(' ')).toThrow('Invalid path: path cannot be empty');
});

it('should not throw for valid paths', () => {
expect(() => validatePath('/tmp/project')).not.toThrow();
expect(() => validatePath('./local/dir')).not.toThrow();
});
});

describe('Scanner Security', () => {
it('should reject malicious paths in scan()', async () => {
try {
await scan('/etc/passwd\0');
expect(true).toBe(false); // Should not reach here
} catch (e) {
expect(e.message).toBe('Invalid path: null byte detected');
}
});
});
Loading