|
| 1 | +import { describe, expect, it } from 'vitest' |
| 2 | +import { createHttpResponseFromBlock } from '@/lib/workflows/utils' |
| 3 | + |
| 4 | +describe('workflow HTTP response safety', () => { |
| 5 | + it.each([ |
| 6 | + { 'Content-Type': 'text/html' }, |
| 7 | + { 'content-type': 'text/html' }, |
| 8 | + { 'CoNtEnT-TyPe': 'image/svg+xml' }, |
| 9 | + { 'Content-Type': 'application/json', 'content-type': 'text/html' }, |
| 10 | + ])('keeps untrusted markup as JSON with headers %j', async (headers) => { |
| 11 | + const data = { message: '<script>alert(document.domain)</script>' } |
| 12 | + const response = await createHttpResponseFromBlock({ |
| 13 | + output: { |
| 14 | + data, |
| 15 | + status: 201, |
| 16 | + headers: { |
| 17 | + ...headers, |
| 18 | + 'X-Content-Type-Options': 'invalid', |
| 19 | + 'X-API-Version': '1.0', |
| 20 | + 'Cache-Control': 'no-cache', |
| 21 | + 'Retry-After': '30', |
| 22 | + }, |
| 23 | + }, |
| 24 | + }) |
| 25 | + |
| 26 | + expect(response.headers.get('content-type')).toBe('application/json') |
| 27 | + expect(response.headers.get('x-content-type-options')).toBe('nosniff') |
| 28 | + expect(response.status).toBe(201) |
| 29 | + expect(response.headers.get('x-api-version')).toBe('1.0') |
| 30 | + expect(response.headers.get('cache-control')).toBe('no-cache') |
| 31 | + expect(response.headers.get('retry-after')).toBe('30') |
| 32 | + expect(await response.json()).toEqual(data) |
| 33 | + }) |
| 34 | + |
| 35 | + it('does not let workflow output set cookies, browser policies, redirects, or transport headers', async () => { |
| 36 | + const response = await createHttpResponseFromBlock({ |
| 37 | + output: { |
| 38 | + data: { message: 'complete' }, |
| 39 | + status: 200, |
| 40 | + headers: { |
| 41 | + 'SeT-CoOkIe': 'session=untrusted; Path=/', |
| 42 | + 'Set-Cookie2': 'session=untrusted', |
| 43 | + 'Content-Disposition': 'inline', |
| 44 | + 'Content-Security-Policy': "default-src * 'unsafe-inline'", |
| 45 | + 'Content-Security-Policy-Report-Only': 'report-uri /untrusted', |
| 46 | + 'X-Frame-Options': 'ALLOWALL', |
| 47 | + 'X-XSS-Protection': '0', |
| 48 | + 'X-Download-Options': 'untrusted', |
| 49 | + 'X-DNS-Prefetch-Control': 'on', |
| 50 | + 'X-Permitted-Cross-Domain-Policies': 'all', |
| 51 | + 'X-UA-Compatible': 'IE=7', |
| 52 | + 'X-WebKit-CSP': "default-src * 'unsafe-inline'", |
| 53 | + 'X-Content-Security-Policy': "default-src * 'unsafe-inline'", |
| 54 | + 'Accept-CH': 'Sec-CH-UA-Model', |
| 55 | + 'Accept-CH-Lifetime': '86400', |
| 56 | + 'Critical-CH': 'Sec-CH-UA-Model', |
| 57 | + 'Public-Key-Pins': 'max-age=0', |
| 58 | + 'Public-Key-Pins-Report-Only': 'max-age=0; report-uri="/untrusted"', |
| 59 | + 'Access-Control-Allow-Origin': '*', |
| 60 | + 'Access-Control-Allow-Credentials': 'true', |
| 61 | + 'Cross-Origin-Resource-Policy': 'cross-origin', |
| 62 | + 'Clear-Site-Data': '"*"', |
| 63 | + 'Permissions-Policy': 'camera=*', |
| 64 | + 'Document-Policy': 'force-load-at-top', |
| 65 | + 'Referrer-Policy': 'unsafe-url', |
| 66 | + 'Strict-Transport-Security': 'max-age=0', |
| 67 | + 'Origin-Agent-Cluster': '?0', |
| 68 | + Location: '/untrusted', |
| 69 | + Refresh: '0; url=/untrusted', |
| 70 | + Link: '</untrusted>; rel=preload; as=script', |
| 71 | + 'Report-To': '{"group":"untrusted"}', |
| 72 | + 'Reporting-Endpoints': 'default="/untrusted"', |
| 73 | + NEL: '{"report_to":"untrusted","max_age":3600}', |
| 74 | + 'Content-Length': '1', |
| 75 | + 'Content-Encoding': 'gzip', |
| 76 | + 'Transfer-Encoding': 'chunked', |
| 77 | + Connection: 'close', |
| 78 | + 'X-Middleware-Rewrite': '/untrusted', |
| 79 | + 'X-Accel-Redirect': '/untrusted', |
| 80 | + 'X-Sendfile': '/untrusted', |
| 81 | + }, |
| 82 | + }, |
| 83 | + }) |
| 84 | + |
| 85 | + expect(Object.fromEntries(response.headers)).toEqual({ |
| 86 | + 'content-type': 'application/json', |
| 87 | + 'x-content-type-options': 'nosniff', |
| 88 | + }) |
| 89 | + expect(await response.json()).toEqual({ message: 'complete' }) |
| 90 | + }) |
| 91 | +}) |
0 commit comments