mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-19 09:30:49 +08:00
fix(api-block): stop spoofing a browser fingerprint on outbound HTTP requests (#5496)
* fix(api-block): stop spoofing a browser fingerprint on outbound HTTP requests getDefaultHeaders() sent a Chrome User-Agent, a Referer pointing at Sim's own app, and mismatched Sec-Ch-Ua client hints on every API block request. Atlassian's Jira/Confluence Cloud REST API (and other browser-aware anti-CSRF/bot-defense layers) reject requests carrying a browser User-Agent with 403 "XSRF check failed", even with a valid Basic-auth header and X-Atlassian-Token: no-check set. Default headers now identify honestly as Sim, matching how other HTTP clients (curl, Postman, axios) behave. User-supplied headers still override any default. * fix(testing): sync shared tool-tester mock headers with new defaults createMockHeaders in the shared @sim/testing tool-tester builder still hardcoded the old Chrome UA / Referer / Sec-Ch-Ua fallback values. It was unreachable in current tests but would silently diverge from production getDefaultHeaders() for any future test hitting its fallback path.
This commit is contained in:
@@ -119,17 +119,7 @@ describe('HTTP Request Tool', () => {
|
||||
expect(headers2['content-type']).toBe('text/plain')
|
||||
})
|
||||
|
||||
it('should set dynamic Referer header correctly', async () => {
|
||||
const originalWindow = global.window
|
||||
Object.defineProperty(global, 'window', {
|
||||
value: {
|
||||
location: {
|
||||
origin: 'https://sim.ai',
|
||||
},
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
|
||||
it('should not set a default Referer header', async () => {
|
||||
tester.setup(mockHttpResponses.simple)
|
||||
|
||||
await tester.execute({
|
||||
@@ -138,9 +128,20 @@ describe('HTTP Request Tool', () => {
|
||||
})
|
||||
|
||||
const fetchCall = (global.fetch as any).mock.calls[0]
|
||||
expect(fetchCall[1].headers.Referer).toBe('https://sim.ai')
|
||||
expect(fetchCall[1].headers.Referer).toBeUndefined()
|
||||
})
|
||||
|
||||
global.window = originalWindow
|
||||
it('should respect a user-provided Referer header', async () => {
|
||||
tester.setup(mockHttpResponses.simple)
|
||||
|
||||
await tester.execute({
|
||||
url: 'https://api.example.com',
|
||||
method: 'GET',
|
||||
headers: [{ cells: { Key: 'Referer', Value: 'https://custom.example.com' } }],
|
||||
})
|
||||
|
||||
const fetchCall = (global.fetch as any).mock.calls[0]
|
||||
expect(fetchCall[1].headers.Referer).toBe('https://custom.example.com')
|
||||
})
|
||||
|
||||
it('should set dynamic Host header correctly', async () => {
|
||||
@@ -193,16 +194,6 @@ describe('HTTP Request Tool', () => {
|
||||
it('should apply default and dynamic headers to requests', async () => {
|
||||
tester.setup(mockHttpResponses.simple)
|
||||
|
||||
const originalWindow = global.window
|
||||
Object.defineProperty(global, 'window', {
|
||||
value: {
|
||||
location: {
|
||||
origin: 'https://sim.ai',
|
||||
},
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
|
||||
await tester.execute({
|
||||
url: 'https://api.example.com/data',
|
||||
method: 'GET',
|
||||
@@ -212,15 +203,13 @@ describe('HTTP Request Tool', () => {
|
||||
const headers = fetchCall[1].headers
|
||||
|
||||
expect(headers.Host).toBe('api.example.com')
|
||||
expect(headers.Referer).toBe('https://sim.ai')
|
||||
expect(headers['User-Agent']).toContain('Mozilla')
|
||||
expect(headers.Referer).toBeUndefined()
|
||||
expect(headers['User-Agent']).toBe('Sim/1.0 (+https://sim.ai)')
|
||||
expect(headers.Accept).toBe('*/*')
|
||||
expect(headers['Accept-Encoding']).toContain('gzip')
|
||||
expect(headers['Cache-Control']).toBe('no-cache')
|
||||
expect(headers.Connection).toBe('keep-alive')
|
||||
expect(headers['Sec-Ch-Ua']).toContain('Chromium')
|
||||
|
||||
global.window = originalWindow
|
||||
expect(headers['Sec-Ch-Ua']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle successful GET requests', async () => {
|
||||
@@ -434,16 +423,6 @@ describe('HTTP Request Tool', () => {
|
||||
it('should apply all default headers correctly', async () => {
|
||||
tester.setup(mockHttpResponses.simple)
|
||||
|
||||
const originalWindow = global.window
|
||||
Object.defineProperty(global, 'window', {
|
||||
value: {
|
||||
location: {
|
||||
origin: 'https://sim.ai',
|
||||
},
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
|
||||
await tester.execute({
|
||||
url: 'https://api.example.com/data',
|
||||
method: 'GET',
|
||||
@@ -452,18 +431,16 @@ describe('HTTP Request Tool', () => {
|
||||
const fetchCall = (global.fetch as any).mock.calls[0]
|
||||
const headers = fetchCall[1].headers
|
||||
|
||||
expect(headers['User-Agent']).toMatch(/Mozilla\/5\.0.*Chrome.*Safari/)
|
||||
expect(headers['User-Agent']).toBe('Sim/1.0 (+https://sim.ai)')
|
||||
expect(headers.Accept).toBe('*/*')
|
||||
expect(headers['Accept-Encoding']).toBe('gzip, deflate, br')
|
||||
expect(headers['Cache-Control']).toBe('no-cache')
|
||||
expect(headers.Connection).toBe('keep-alive')
|
||||
expect(headers['Sec-Ch-Ua']).toMatch(/Chromium.*Not-A\.Brand/)
|
||||
expect(headers['Sec-Ch-Ua-Mobile']).toBe('?0')
|
||||
expect(headers['Sec-Ch-Ua-Platform']).toBe('"macOS"')
|
||||
expect(headers.Referer).toBe('https://sim.ai')
|
||||
expect(headers['Sec-Ch-Ua']).toBeUndefined()
|
||||
expect(headers['Sec-Ch-Ua-Mobile']).toBeUndefined()
|
||||
expect(headers['Sec-Ch-Ua-Platform']).toBeUndefined()
|
||||
expect(headers.Referer).toBeUndefined()
|
||||
expect(headers.Host).toBe('api.example.com')
|
||||
|
||||
global.window = originalWindow
|
||||
})
|
||||
|
||||
it('should allow overriding default headers', async () => {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { transformTable } from '@/tools/shared/table'
|
||||
import type { TableRow } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Creates a set of default headers used in HTTP requests
|
||||
* Creates a set of default headers used in HTTP requests.
|
||||
*
|
||||
* Identifies as Sim rather than impersonating a browser. Browser-fingerprint
|
||||
* headers (Referer, Sec-Ch-Ua*) trip anti-CSRF/bot-defense heuristics on
|
||||
* providers like Atlassian, which reject REST calls carrying a browser
|
||||
* User-Agent regardless of X-Atlassian-Token. See
|
||||
* https://support.atlassian.com/jira/kb/rest-api-calls-with-a-browser-user-agent-header-may-fail-csrf-checks/
|
||||
* @param customHeaders Additional user-provided headers to include
|
||||
* @param url Target URL for the request (used for setting Host header)
|
||||
* @returns Record of HTTP headers
|
||||
@@ -13,16 +18,11 @@ export const getDefaultHeaders = (
|
||||
url?: string
|
||||
): Record<string, string> => {
|
||||
const headers: Record<string, string> = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
||||
'User-Agent': 'Sim/1.0 (+https://sim.ai)',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
Referer: getBaseUrl(),
|
||||
'Sec-Ch-Ua': 'Chromium;v=91, Not-A.Brand;v=99',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"macOS"',
|
||||
...customHeaders,
|
||||
}
|
||||
|
||||
|
||||
@@ -43,16 +43,11 @@ export interface ToolResponse {
|
||||
*/
|
||||
const createMockHeaders = (customHeaders: Record<string, string> = {}) => {
|
||||
return {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
||||
'User-Agent': 'Sim/1.0 (+https://sim.ai)',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
Referer: 'https://www.sim.ai',
|
||||
'Sec-Ch-Ua': 'Chromium;v=91, Not-A.Brand;v=99',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"macOS"',
|
||||
...customHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user