mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 22:36:19 +00:00
chore(stack-files): client-side path-traversal guard in stackFilesApi (#1190)
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.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user