mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-27 20:29:10 +00:00
82a4e94589
The backend already rejects path-traversal attempts through isValidRelativeStackPath, so the server side is safe today. Adding a client-side mirror is defense-in-depth: it shortens the failure loop (no wasted round trip) and protects against a future server-side regression that loosens validation. Adds isClientSafeRelPath in frontend/src/lib/stackFilesApi.ts mirroring the backend predicate (rejects absolute paths, drive letters, backslashes, NUL bytes, double slashes, and any segment that is `.` or `..`). Wraps every export that accepts a relPath / targetDir / fromRel / toRel argument with assertSafeRelPath, throwing a clear Error before the fetch is issued. 12 unit tests cover the predicate (POSIX accepts, traversal rejects, Windows drive letters, backslashes, NUL bytes, non-string inputs). Frontend suite stays at 288/288.
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
/**
|
|
* Unit tests for the client-side path-traversal guard added to
|
|
* stackFilesApi exports. The guard mirrors
|
|
* backend/src/utils/validation.ts::isValidRelativeStackPath so a
|
|
* malicious or buggy caller cannot slip a `..` segment past the
|
|
* client before it would otherwise be caught by the server.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { isClientSafeRelPath } from '../stackFilesApi';
|
|
|
|
describe('isClientSafeRelPath', () => {
|
|
it('accepts the empty string (means the stack root)', () => {
|
|
expect(isClientSafeRelPath('')).toBe(true);
|
|
});
|
|
|
|
it('accepts a simple file name', () => {
|
|
expect(isClientSafeRelPath('compose.yaml')).toBe(true);
|
|
});
|
|
|
|
it('accepts a nested POSIX path', () => {
|
|
expect(isClientSafeRelPath('config/redis/redis.conf')).toBe(true);
|
|
});
|
|
|
|
it('accepts a hidden file', () => {
|
|
expect(isClientSafeRelPath('.env')).toBe(true);
|
|
});
|
|
|
|
it('rejects parent-directory traversal', () => {
|
|
expect(isClientSafeRelPath('..')).toBe(false);
|
|
expect(isClientSafeRelPath('../etc/passwd')).toBe(false);
|
|
expect(isClientSafeRelPath('config/../../../etc/passwd')).toBe(false);
|
|
});
|
|
|
|
it('rejects same-directory segment', () => {
|
|
expect(isClientSafeRelPath('./config')).toBe(false);
|
|
expect(isClientSafeRelPath('config/./redis.conf')).toBe(false);
|
|
});
|
|
|
|
it('rejects absolute POSIX paths', () => {
|
|
expect(isClientSafeRelPath('/etc/passwd')).toBe(false);
|
|
expect(isClientSafeRelPath('/')).toBe(false);
|
|
});
|
|
|
|
it('rejects Windows drive-letter paths', () => {
|
|
expect(isClientSafeRelPath('C:/Windows/System32')).toBe(false);
|
|
expect(isClientSafeRelPath('d:foo')).toBe(false);
|
|
});
|
|
|
|
it('rejects backslashes', () => {
|
|
expect(isClientSafeRelPath('config\\redis.conf')).toBe(false);
|
|
expect(isClientSafeRelPath('..\\..\\etc\\passwd')).toBe(false);
|
|
});
|
|
|
|
it('rejects NUL bytes', () => {
|
|
expect(isClientSafeRelPath('foo\0bar')).toBe(false);
|
|
});
|
|
|
|
it('rejects double slashes', () => {
|
|
expect(isClientSafeRelPath('config//redis.conf')).toBe(false);
|
|
});
|
|
|
|
it('rejects non-string input', () => {
|
|
expect(isClientSafeRelPath(undefined as unknown as string)).toBe(false);
|
|
expect(isClientSafeRelPath(null as unknown as string)).toBe(false);
|
|
expect(isClientSafeRelPath(42 as unknown as string)).toBe(false);
|
|
});
|
|
});
|