/** * R2-002 regression: the OAuth callback reflects untrusted query params and * upstream response bodies into HTML. Every untrusted value must be * HTML-escaped (via escapeXml) before interpolation so a quote/`'; const descParam = '">'; const res = await server.inject({ method: 'GET', url: `/api/oauth/github/callback?error=${encodeURIComponent(errorParam)}&error_description=${encodeURIComponent(descParam)}`, }); expect(res.statusCode).toBe(200); const body = res.body; // Raw payload markup must NOT appear verbatim. expect(body).not.toContain(''); expect(body).not.toContain(''); // Escaped forms must be present instead. expect(body).toContain('<script>alert(1)</script>'); expect(body).toContain('<img src=x onerror=alert(2)>'); expect(body).toContain('">'); }); it('does not double-escape benign error descriptions', async () => { server = createTestServer(); const res = await server.inject({ method: 'GET', url: '/api/oauth/github/callback?error=access_denied&error_description=user%20declined', }); expect(res.statusCode).toBe(200); expect(res.body).toContain('access_denied: user declined'); }); });