diff --git a/src/routes/__tests__/screenshot.test.ts b/src/routes/__tests__/screenshot.test.ts index 6a03b58..5422831 100644 --- a/src/routes/__tests__/screenshot.test.ts +++ b/src/routes/__tests__/screenshot.test.ts @@ -1086,4 +1086,115 @@ describe('Screenshot Route', () => { ) }) }) + + describe('userAgent parameter', () => { + it('should return 400 when userAgent exceeds 500 characters', async () => { + const req = createMockRequest({ + url: "https://example.com", + userAgent: "A".repeat(501) + }) + const res = createMockResponse() + + const handler = screenshotRouter.stack.find(layer => + layer.route?.methods.post + )?.route.stack[0].handle + await handler(req, res, vi.fn()) + + expect(res.status).toHaveBeenCalledWith(400) + expect(res.json).toHaveBeenCalledWith({ + error: "userAgent: maximum 500 characters allowed" + }) + }) + + it('should return 400 when userAgent contains newline (\\n)', async () => { + const req = createMockRequest({ + url: "https://example.com", + userAgent: "Mozilla/5.0\nX-Injected: true" + }) + const res = createMockResponse() + + const handler = screenshotRouter.stack.find(layer => + layer.route?.methods.post + )?.route.stack[0].handle + await handler(req, res, vi.fn()) + + expect(res.status).toHaveBeenCalledWith(400) + expect(res.json).toHaveBeenCalledWith({ + error: "userAgent: newlines are not allowed (HTTP header injection prevention)" + }) + }) + + it('should return 400 when userAgent contains carriage return (\\r)', async () => { + const req = createMockRequest({ + url: "https://example.com", + userAgent: "Mozilla/5.0\rX-Injected: true" + }) + const res = createMockResponse() + + const handler = screenshotRouter.stack.find(layer => + layer.route?.methods.post + )?.route.stack[0].handle + await handler(req, res, vi.fn()) + + expect(res.status).toHaveBeenCalledWith(400) + expect(res.json).toHaveBeenCalledWith({ + error: "userAgent: newlines are not allowed (HTTP header injection prevention)" + }) + }) + + it('should accept valid userAgent at exactly 500 characters', async () => { + const req = createMockRequest({ + url: "https://example.com", + userAgent: "A".repeat(500) + }) + const res = createMockResponse() + + const handler = screenshotRouter.stack.find(layer => + layer.route?.methods.post + )?.route.stack[0].handle + await handler(req, res, vi.fn()) + + expect(res.status).not.toHaveBeenCalledWith(400) + }) + + it('should pass userAgent to takeScreenshot service', async () => { + const req = createMockRequest({ + url: "https://example.com", + userAgent: "Mozilla/5.0 (compatible; SnapAPI/1.0)" + }) + const res = createMockResponse() + + const handler = screenshotRouter.stack.find(layer => + layer.route?.methods.post + )?.route.stack[0].handle + await handler(req, res, vi.fn()) + + expect(mockTakeScreenshot).toHaveBeenCalledWith( + expect.objectContaining({ + userAgent: "Mozilla/5.0 (compatible; SnapAPI/1.0)" + }) + ) + }) + + it('should accept userAgent via GET query parameter', async () => { + const req = createMockRequest({ + url: "https://example.com", + userAgent: "CustomBot/2.0" + }) + req.method = "GET" + req.query = req.body + const res = createMockResponse() + + const handler = screenshotRouter.stack.find(layer => + layer.route?.methods.get + )?.route.stack[0].handle + await handler(req, res, vi.fn()) + + expect(mockTakeScreenshot).toHaveBeenCalledWith( + expect.objectContaining({ + userAgent: "CustomBot/2.0" + }) + ) + }) + }) }) \ No newline at end of file diff --git a/src/routes/screenshot.ts b/src/routes/screenshot.ts index d128ef4..0461782 100644 --- a/src/routes/screenshot.ts +++ b/src/routes/screenshot.ts @@ -445,6 +445,18 @@ async function handleScreenshotRequest(req: any, res: any) { return; } + // Validate userAgent parameter + if (userAgent && typeof userAgent === 'string') { + if (userAgent.length > 500) { + res.status(400).json({ error: "userAgent: maximum 500 characters allowed" }); + return; + } + if (/[\r\n]/.test(userAgent)) { + res.status(400).json({ error: "userAgent: newlines are not allowed (HTTP header injection prevention)" }); + return; + } + } + // Validate css parameter if (css && typeof css === 'string' && css.length > 5000) { res.status(400).json({ error: "css: maximum 5000 characters allowed" });