because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(security): enforce TLS on DATABASE_URL in prod (CVG-59) and magic-byte upload check (CVG-58)

CVG-59: Zod refinement on apps/api env validator; mirror in apps/worker. Production must include sslmode=require/verify-ca/verify-full unless ALLOWED_DB_PLAINTEXT=true (escape hatch for Cloud SQL Auth Proxy etc). Dev unchanged. CVG-58: validateMagicBytes() in @cv/file-upload checks the first bytes of the buffer match the declared MIME (%PDF for application/pdf, PK\x03\x04 for DOCX, valid UTF-8 for text/markdown). Wired into validateFile() so MIME spoofing fails fast before pdf-parse/mammoth see the bytes. 10 new tests.

+210 -1
+25
apps/api/src/config/env.validation.ts
··· 20 20 .url() 21 21 .default("postgresql://cv:cv@localhost:5432/cv"), 22 22 23 + // ALLOWED_DB_PLAINTEXT escape hatch for hosted Postgres providers that 24 + // terminate TLS at a sidecar (Cloud SQL Auth Proxy, RDS Proxy in some 25 + // configs). Off by default - production must use sslmode=require or stricter. 26 + ALLOWED_DB_PLAINTEXT: z 27 + .string() 28 + .default("false") 29 + .transform((v) => v === "true"), 30 + 23 31 // Server Configuration 24 32 PORT: z.coerce.number().default(3000), 25 33 SERVER_PORT: z.coerce.number().default(3000), ··· 126 134 path: [key], 127 135 }); 128 136 } 137 + } 138 + } 139 + 140 + // Production must encrypt the DB connection. The hosted-Postgres escape 141 + // hatch (ALLOWED_DB_PLAINTEXT=true) is for providers that terminate TLS 142 + // at a sidecar; default-deny otherwise. 143 + if (data.NODE_ENV === "production" && !data.ALLOWED_DB_PLAINTEXT) { 144 + const hasTls = /[?&]sslmode=(require|verify-ca|verify-full)\b/.test( 145 + data.DATABASE_URL, 146 + ); 147 + if (!hasTls) { 148 + ctx.addIssue({ 149 + code: z.ZodIssueCode.custom, 150 + message: 151 + "DATABASE_URL must include sslmode=require (or verify-ca / verify-full) in production. Set ALLOWED_DB_PLAINTEXT=true only if your hosted Postgres terminates TLS at a sidecar (e.g. Cloud SQL Auth Proxy).", 152 + path: ["DATABASE_URL"], 153 + }); 129 154 } 130 155 } 131 156 });
+16 -1
apps/worker/src/config.ts
··· 6 6 return value; 7 7 }; 8 8 9 + const requireDatabaseUrlWithTls = (): string => { 10 + const url = requireEnv("DATABASE_URL"); 11 + const isProduction = process.env["NODE_ENV"] === "production"; 12 + const allowPlaintext = process.env["ALLOWED_DB_PLAINTEXT"] === "true"; 13 + if (!isProduction || allowPlaintext) { 14 + return url; 15 + } 16 + if (!/[?&]sslmode=(require|verify-ca|verify-full)\b/.test(url)) { 17 + throw new Error( 18 + "DATABASE_URL must include sslmode=require (or verify-ca / verify-full) in production. Set ALLOWED_DB_PLAINTEXT=true only if your hosted Postgres terminates TLS at a sidecar.", 19 + ); 20 + } 21 + return url; 22 + }; 23 + 9 24 export type WorkerConfig = typeof config; 10 25 11 26 export const config = { 12 27 get databaseUrl() { 13 - return requireEnv("DATABASE_URL"); 28 + return requireDatabaseUrlWithTls(); 14 29 }, 15 30 queueSchema: process.env["QUEUE_SCHEMA"] ?? "queue", 16 31 queueName: process.env["QUEUE_NAME"] ?? "default",
+81
packages/file-upload/src/__tests__/validators.spec.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { SupportedMimeTypes } from "../types"; 3 + import { validateMagicBytes } from "../validators"; 4 + 5 + const pdfBytes = Buffer.from("%PDF-1.4\n…rest of pdf…"); 6 + const docxBytes = Buffer.concat([ 7 + Buffer.from([0x50, 0x4b, 0x03, 0x04]), 8 + Buffer.from("…rest of zip…"), 9 + ]); 10 + const utf8Bytes = Buffer.from("hello world\n"); 11 + const invalidUtf8 = Buffer.from([0xff, 0xfe, 0xfd]); 12 + const pngBytes = Buffer.from([ 13 + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 14 + ]); 15 + const empty = Buffer.alloc(0); 16 + 17 + describe("validateMagicBytes", () => { 18 + describe("PDF", () => { 19 + it("accepts a buffer starting with %PDF", () => { 20 + expect(validateMagicBytes(pdfBytes, SupportedMimeTypes.PDF)).toEqual({ 21 + valid: true, 22 + }); 23 + }); 24 + 25 + it("accepts %PDF anywhere in the first 1024 bytes", () => { 26 + const padded = Buffer.concat([Buffer.alloc(500, 0), pdfBytes]); 27 + expect(validateMagicBytes(padded, SupportedMimeTypes.PDF)).toEqual({ 28 + valid: true, 29 + }); 30 + }); 31 + 32 + it("rejects a buffer claiming PDF but containing PNG bytes", () => { 33 + const result = validateMagicBytes(pngBytes, SupportedMimeTypes.PDF); 34 + expect(result.valid).toBe(false); 35 + }); 36 + 37 + it("rejects an empty buffer", () => { 38 + const result = validateMagicBytes(empty, SupportedMimeTypes.PDF); 39 + expect(result.valid).toBe(false); 40 + }); 41 + }); 42 + 43 + describe("DOCX", () => { 44 + it("accepts a buffer starting with PK signature", () => { 45 + expect(validateMagicBytes(docxBytes, SupportedMimeTypes.DOCX)).toEqual({ 46 + valid: true, 47 + }); 48 + }); 49 + 50 + it("rejects a buffer claiming DOCX but containing PDF bytes", () => { 51 + const result = validateMagicBytes(pdfBytes, SupportedMimeTypes.DOCX); 52 + expect(result.valid).toBe(false); 53 + }); 54 + 55 + it("rejects PNG bytes", () => { 56 + const result = validateMagicBytes(pngBytes, SupportedMimeTypes.DOCX); 57 + expect(result.valid).toBe(false); 58 + }); 59 + }); 60 + 61 + describe("TXT / MD", () => { 62 + it("accepts valid UTF-8 text", () => { 63 + expect(validateMagicBytes(utf8Bytes, SupportedMimeTypes.TXT)).toEqual({ 64 + valid: true, 65 + }); 66 + expect(validateMagicBytes(utf8Bytes, SupportedMimeTypes.MD)).toEqual({ 67 + valid: true, 68 + }); 69 + }); 70 + 71 + it("rejects invalid UTF-8 byte sequences", () => { 72 + const result = validateMagicBytes(invalidUtf8, SupportedMimeTypes.TXT); 73 + expect(result.valid).toBe(false); 74 + }); 75 + 76 + it("rejects empty buffer", () => { 77 + const result = validateMagicBytes(empty, SupportedMimeTypes.TXT); 78 + expect(result.valid).toBe(false); 79 + }); 80 + }); 81 + });
+88
packages/file-upload/src/validators.ts
··· 4 4 const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB 5 5 6 6 /** 7 + * Magic-byte signatures for the supported MIME types. The client-supplied 8 + * Content-Type is trivially spoofable; the actual file bytes are not. 9 + * 10 + * - PDF: "%PDF" header (some files have leading whitespace; scan first 1024 bytes) 11 + * - DOCX: ZIP local file header "PK\x03\x04" (DOCX is a ZIP archive) 12 + * - TXT/MD: no signature; verify it parses as valid UTF-8 instead 13 + */ 14 + const PDF_SIGNATURE = Buffer.from("%PDF"); 15 + const ZIP_SIGNATURE = Buffer.from([0x50, 0x4b, 0x03, 0x04]); 16 + const ZIP_EMPTY_SIGNATURE = Buffer.from([0x50, 0x4b, 0x05, 0x06]); 17 + const ZIP_SPANNED_SIGNATURE = Buffer.from([0x50, 0x4b, 0x07, 0x08]); 18 + 19 + const startsWith = (haystack: Buffer, needle: Buffer): boolean => 20 + haystack.length >= needle.length && 21 + haystack.subarray(0, needle.length).equals(needle); 22 + 23 + const PDF_SCAN_WINDOW = 1024; 24 + 25 + const containsPdfSignature = (buffer: Buffer): boolean => { 26 + // Some PDFs are wrapped in mail attachments or have a BOM; permit %PDF 27 + // anywhere in the first 1KB. Outside that window it's almost certainly 28 + // a content-injection attempt. 29 + return buffer.subarray(0, PDF_SCAN_WINDOW).indexOf(PDF_SIGNATURE) !== -1; 30 + }; 31 + 32 + const startsWithAnyZipSignature = (buffer: Buffer): boolean => 33 + startsWith(buffer, ZIP_SIGNATURE) || 34 + startsWith(buffer, ZIP_EMPTY_SIGNATURE) || 35 + startsWith(buffer, ZIP_SPANNED_SIGNATURE); 36 + 37 + const isValidUtf8 = (buffer: Buffer): boolean => { 38 + try { 39 + // Strict mode: `fatal: true` throws on invalid sequences. 40 + new TextDecoder("utf-8", { fatal: true }).decode(buffer); 41 + return true; 42 + } catch { 43 + return false; 44 + } 45 + }; 46 + 47 + /** 48 + * Verify the actual file bytes match the declared MIME type. Defense against 49 + * an attacker uploading evil.exe with Content-Type: application/pdf and 50 + * watching it sail through into pdf-parse. 51 + */ 52 + export const validateMagicBytes = ( 53 + buffer: Buffer, 54 + mimeType: SupportedMimeType, 55 + ): FileValidationResult => { 56 + if (buffer.length === 0) { 57 + return { valid: false, error: "File is empty" }; 58 + } 59 + 60 + if (mimeType === SupportedMimeTypes.PDF) { 61 + return containsPdfSignature(buffer) 62 + ? { valid: true } 63 + : { valid: false, error: "File contents do not look like a PDF" }; 64 + } 65 + 66 + if (mimeType === SupportedMimeTypes.DOCX) { 67 + return startsWithAnyZipSignature(buffer) 68 + ? { valid: true } 69 + : { valid: false, error: "File contents do not look like a DOCX (ZIP archive)" }; 70 + } 71 + 72 + if ( 73 + mimeType === SupportedMimeTypes.TXT || 74 + mimeType === SupportedMimeTypes.MD 75 + ) { 76 + return isValidUtf8(buffer) 77 + ? { valid: true } 78 + : { valid: false, error: "File is not valid UTF-8 text" }; 79 + } 80 + 81 + return { valid: false, error: `No magic-byte rule for MIME ${mimeType}` }; 82 + }; 83 + 84 + /** 7 85 * Validate that MIME type is supported 8 86 */ 9 87 export const isSupportedMimeType = (mimeType: string): boolean => ··· 93 171 94 172 if (!file.buffer) { 95 173 return { valid: false, error: 'File buffer is required' }; 174 + } 175 + 176 + // Magic-byte check: declared MIME has been validated above; now confirm 177 + // the actual bytes match. Defense against MIME spoofing. 178 + const magicBytesValidation = validateMagicBytes( 179 + file.buffer, 180 + file.mimeType as SupportedMimeType, 181 + ); 182 + if (!magicBytesValidation.valid) { 183 + return magicBytesValidation; 96 184 } 97 185 98 186 try {