Skip to content

Commit d767d76

Browse files
waleedlatif1claude
andcommitted
fix(sap_s4hana): preserve raw Set-Cookie array for CSRF cookie join
SecureFetchHeaders previously collapsed multi-value Set-Cookie headers with ", ", forcing consumers to re-split via a fragile regex. Cookie values containing "=" or "," (e.g., Base64 session tokens) could be misparsed and produce malformed Cookie strings on CSRF-protected mutations. Add SecureFetchHeaders.getSetCookie() that returns the raw array, and update the S/4HANA OData proxy's joinSetCookies to consume it directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d7c9f9c commit d767d76

2 files changed

Lines changed: 24 additions & 7 deletions

File tree

apps/sim/app/api/tools/sap_s4hana/proxy/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ interface CsrfBundle {
113113
}
114114

115115
function joinSetCookies(response: SecureFetchResponse): string {
116-
const cookies = (response.headers.get('set-cookie') ?? '').split(/,\s*(?=[^=,;\s]+=)/)
117-
return cookies
116+
return response.headers
117+
.getSetCookie()
118118
.map((c) => c.split(';')[0]?.trim())
119119
.filter(Boolean)
120120
.join('; ')

apps/sim/lib/core/security/input-validation.server.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,15 +217,22 @@ export interface SecureFetchOptions {
217217

218218
export class SecureFetchHeaders {
219219
private headers: Map<string, string>
220+
private setCookies: string[]
220221

221-
constructor(headers: Record<string, string>) {
222+
constructor(headers: Record<string, string>, setCookies: string[] = []) {
222223
this.headers = new Map(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]))
224+
this.setCookies = setCookies
223225
}
224226

225227
get(name: string): string | null {
226228
return this.headers.get(name.toLowerCase()) ?? null
227229
}
228230

231+
/** Returns the raw `Set-Cookie` header values as an array. Each entry is one cookie. */
232+
getSetCookie(): string[] {
233+
return [...this.setCookies]
234+
}
235+
229236
toRecord(): Record<string, string> {
230237
const record: Record<string, string> = {}
231238
for (const [key, value] of this.headers) {
@@ -384,19 +391,29 @@ export async function secureFetchWithPinnedIP(
384391
const bodyBuffer = Buffer.concat(chunks)
385392
const body = bodyBuffer.toString('utf-8')
386393
const headersRecord: Record<string, string> = {}
394+
let setCookieArray: string[] = []
387395
for (const [key, value] of Object.entries(res.headers)) {
388-
if (typeof value === 'string') {
389-
headersRecord[key.toLowerCase()] = value
396+
const lowerKey = key.toLowerCase()
397+
if (lowerKey === 'set-cookie') {
398+
if (Array.isArray(value)) {
399+
setCookieArray = value
400+
headersRecord[lowerKey] = value.join(', ')
401+
} else if (typeof value === 'string') {
402+
setCookieArray = [value]
403+
headersRecord[lowerKey] = value
404+
}
405+
} else if (typeof value === 'string') {
406+
headersRecord[lowerKey] = value
390407
} else if (Array.isArray(value)) {
391-
headersRecord[key.toLowerCase()] = value.join(', ')
408+
headersRecord[lowerKey] = value.join(', ')
392409
}
393410
}
394411

395412
settledResolve({
396413
ok: statusCode >= 200 && statusCode < 300,
397414
status: statusCode,
398415
statusText: res.statusMessage || '',
399-
headers: new SecureFetchHeaders(headersRecord),
416+
headers: new SecureFetchHeaders(headersRecord, setCookieArray),
400417
text: async () => body,
401418
json: async () => JSON.parse(body),
402419
arrayBuffer: async () =>

0 commit comments

Comments
 (0)