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:
Waleed
2026-07-07 16:58:13 -07:00
committed by GitHub
parent 619e912256
commit 858f7d2c2c
3 changed files with 31 additions and 59 deletions
+22 -45
View File
@@ -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 () => {
+8 -8
View File
@@ -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,
}
}