diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 501ee8f..03026c1 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -7,3 +7,8 @@ **Vulnerability:** The application used a hardcoded string ('ffui-webui-2025') as a salt for HMAC signatures on session tokens. This makes session tokens predictable if the password is known or weak, and allows rainbow table attacks if the source code is public. **Learning:** Hardcoded salts in open-source projects defeat the purpose of salting. Even if combined with a password, they don't provide per-installation uniqueness. **Prevention:** Use a random secret generated at runtime (or installation time) and persist it in the application configuration. + +## 2026-01-22 - Path Traversal in API Schemas +**Vulnerability:** The `JobStartRequestSchema` validated filenames only by length (`min(1)`), allowing path traversal characters (e.g., `../../etc/passwd`). If the backend naively concatenates this filename to a path, it allows arbitrary file read/write. +**Learning:** Zod's string validation is basic. For file paths, explicit validation against directory traversal (e.g., forbidding `..`) is essential, especially when inputs are passed to filesystem operations. +**Prevention:** Use `.refine()` in Zod schemas to reject strings containing `..` path segments: `/(^|[\/])\.\.([\/]|$)/`. diff --git a/src/main/webui/schemas/__tests__/web-api.schemas.test.ts b/src/main/webui/schemas/__tests__/web-api.schemas.test.ts new file mode 100644 index 0000000..8b08c4a --- /dev/null +++ b/src/main/webui/schemas/__tests__/web-api.schemas.test.ts @@ -0,0 +1,34 @@ + +import { JobStartRequestSchema } from '../web-api.schemas'; + +describe('JobStartRequestSchema', () => { + it('should accept valid filenames', () => { + const validInputs = [ + { filename: 'test.gcode' }, + { filename: 'folder/test.gcode' }, + { filename: 'my_print_job.gcode' }, + { filename: 'job.gcode', startNow: false }, + ]; + + validInputs.forEach((input) => { + const result = JobStartRequestSchema.safeParse(input); + expect(result.success).toBe(true); + }); + }); + + it('should reject path traversal attempts', () => { + const invalidInputs = [ + { filename: '../system.conf' }, + { filename: '../../etc/passwd' }, + { filename: 'folder/../hack.txt' }, + { filename: '/../root.txt' }, + { filename: '..\\windows\\system32' }, + ]; + + invalidInputs.forEach((input) => { + const result = JobStartRequestSchema.safeParse(input); + // We expect validation to fail for these insecure inputs + expect(result.success).toBe(false); + }); + }); +}); diff --git a/src/main/webui/schemas/web-api.schemas.ts b/src/main/webui/schemas/web-api.schemas.ts index acff167..6bd918f 100644 --- a/src/main/webui/schemas/web-api.schemas.ts +++ b/src/main/webui/schemas/web-api.schemas.ts @@ -65,7 +65,10 @@ export const TemperatureDataSchema = z.object({ * Job start data validation */ export const JobStartDataSchema = z.object({ - filename: z.string().min(1), + filename: z + .string() + .min(1) + .refine((val) => !/(^|[\\/])\.\.([\\/]|$)/.test(val), 'Path traversal characters (..) are not allowed'), leveling: z.boolean().optional().default(false), startNow: z.boolean().optional().default(true), }); @@ -93,7 +96,10 @@ const MaterialMappingSchema = z.object({ }); export const JobStartRequestSchema = z.object({ - filename: z.string().min(1, 'Filename is required'), + filename: z + .string() + .min(1, 'Filename is required') + .refine((val) => !/(^|[\\/])\.\.([\\/]|$)/.test(val), 'Path traversal characters (..) are not allowed'), leveling: z.boolean().optional().default(false), startNow: z.boolean().optional().default(true), materialMappings: z