Skip to content

Commit b1c7879

Browse files
fix(oci): reject ambiguous diagnostics
1 parent 77e26a0 commit b1c7879

3 files changed

Lines changed: 65 additions & 15 deletions

File tree

apps/sim/lib/internal/oci/client.server.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ describe('OCI request client', () => {
232232
maxResponseBytes: 65_536,
233233
}).catch((error: unknown) => error)
234234
expect(failure).toBeInstanceOf(OciRequestError)
235-
expect((failure as Error).message).toContain('[REDACTED]')
235+
expect((failure as Error).message).toBe('OCI request failed with status 401')
236236
expect((failure as Error).message).not.toContain('provider-echo')
237237
expect((failure as Error).message).not.toContain('(request-target)')
238238
expect((failure as Error).message).not.toContain('tenant/user/fingerprint')
@@ -298,6 +298,8 @@ describe('OCI request client', () => {
298298
it.each([
299299
encodeURIComponent('-----BEGIN PRIVATE KEY-----\ntruncated'),
300300
encodeURIComponent(`${destination.origin}/n/truncated`),
301+
'----%2DBEGIN PRIVATE KEY-----',
302+
'https:%2F%2Fobjectstorage.us-ashburn-1.oraclecloud.com/n/',
301303
])('fails closed for encoded key or URL prefixes', async (message) => {
302304
secureFetchMock.mockResolvedValueOnce(
303305
secureResponse({
@@ -324,6 +326,8 @@ describe('OCI request client', () => {
324326
'secret-value',
325327
'password-value',
326328
'(request-target) host x-date',
329+
'private-key-value',
330+
'api-key-value',
327331
]
328332
secureFetchMock.mockResolvedValueOnce(
329333
secureResponse({
@@ -337,6 +341,8 @@ describe('OCI request client', () => {
337341
secret: echoedSecrets[2],
338342
'pass%70hrase': echoedSecrets[3],
339343
signing_string: echoedSecrets[4],
344+
'private key': echoedSecrets[5],
345+
'api key': echoedSecrets[6],
340346
}),
341347
}),
342348
})
@@ -352,6 +358,50 @@ describe('OCI request client', () => {
352358
for (const secret of echoedSecrets) expect((failure as Error).message).not.toContain(secret)
353359
})
354360

361+
it('fails closed when structured JSON follows a plain-text prefix', async () => {
362+
const message = `provider failed: ${JSON.stringify({ authorization: 'provider-echo' })}`
363+
secureFetchMock.mockResolvedValueOnce(
364+
secureResponse({
365+
ok: false,
366+
status: 401,
367+
body: JSON.stringify({ code: 'NotAuthenticated', message }),
368+
})
369+
)
370+
const failure = await sendOciRequest({
371+
destination,
372+
credentials,
373+
method: 'GET',
374+
encodedPath: '/n/',
375+
timeout: 10_000,
376+
maxResponseBytes: 65_536,
377+
}).catch((error: unknown) => error)
378+
expect((failure as Error).message).toBe('OCI request failed with status 401')
379+
})
380+
381+
it.each([
382+
`provider failed: ${JSON.stringify(JSON.stringify({ authorization: 'provider-echo' }))}`,
383+
'provider failed: \\"authorization\\":\\"provider-echo\\"',
384+
'signed headers: (request-target) host x-date',
385+
'signed headers: host x-content-sha256',
386+
])('fails closed for escaped structured or signing diagnostics', async (message) => {
387+
secureFetchMock.mockResolvedValueOnce(
388+
secureResponse({
389+
ok: false,
390+
status: 401,
391+
body: JSON.stringify({ code: 'NotAuthenticated', message }),
392+
})
393+
)
394+
const failure = await sendOciRequest({
395+
destination,
396+
credentials,
397+
method: 'GET',
398+
encodedPath: '/n/',
399+
timeout: 10_000,
400+
maxResponseBytes: 65_536,
401+
}).catch((error: unknown) => error)
402+
expect((failure as Error).message).toBe('OCI request failed with status 401')
403+
})
404+
355405
it.each([
356406
'{"authorization":"Signature version=\\"1\\",signature=\\"echoed\\"',
357407
JSON.stringify({ level1: { level2: { level3: { authorization: 'echoed' } } } }),

apps/sim/lib/internal/oci/client.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ function sensitiveRequestValues(
6969
credentials.tenancyId,
7070
credentials.userId,
7171
credentials.fingerprint,
72+
credentials.fingerprint.toUpperCase(),
7273
credentials.privateKey,
7374
credentials.passphrase ?? '',
7475
authorization ?? '',

apps/sim/lib/internal/oci/errors.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const MAX_OCI_ERROR_FIELD_LENGTH = 1024
88
const MAX_OCI_ERROR_INPUT_LENGTH = 65_536
99
const MAX_NESTED_JSON_DEPTH = 3
1010
const OCI_SENSITIVE_JSON_FIELDS = new Set(['signingstring'])
11-
const ENCODED_DIAGNOSTIC_SENTINELS = ['-----BEGIN', 'https://']
1211

1312
function normalizeJsonDiagnosticKey(key: string): string | undefined {
1413
let normalized = key
@@ -31,26 +30,24 @@ function looksLikeStructuredJson(value: string): boolean {
3130

3231
function isSensitiveOciJsonKey(key: string): boolean {
3332
const compactKey = key.replace(/[^a-z]/gi, '').toLowerCase()
34-
return OCI_SENSITIVE_JSON_FIELDS.has(compactKey) || isSensitiveKey(key)
33+
return OCI_SENSITIVE_JSON_FIELDS.has(compactKey) || isSensitiveKey(compactKey)
3534
}
3635

37-
function containsEncodedDiagnosticSentinel(value: string): boolean {
38-
const lowerValue = value.toLowerCase()
39-
return ENCODED_DIAGNOSTIC_SENTINELS.some((sentinel) => {
40-
let encoded = sentinel
41-
for (let depth = 0; depth < MAX_NESTED_JSON_DEPTH; depth += 1) {
42-
encoded = encodeURIComponent(encoded)
43-
if (encoded !== sentinel && lowerValue.includes(encoded.toLowerCase())) return true
44-
}
45-
return false
46-
})
36+
function containsEmbeddedStructuredText(value: string): boolean {
37+
return (
38+
!looksLikeStructuredJson(value) &&
39+
(/[[{]\s*\\*(?:["{[\]}]|-?\d|true\b|false\b|null\b)/.test(value) ||
40+
/\\*"[^"\\\r\n]{1,128}\\*"\s*:\s*/.test(value))
41+
)
4742
}
4843

4944
function flattenJsonDiagnostic(value: unknown, depth = 0): string | undefined {
5045
if (depth > MAX_NESTED_JSON_DEPTH) return undefined
5146
if (value === null) return 'null'
5247
if (typeof value === 'string') {
53-
if (!looksLikeStructuredJson(value)) return value
48+
if (!looksLikeStructuredJson(value)) {
49+
return containsEmbeddedStructuredText(value) ? undefined : value
50+
}
5451
if (depth === MAX_NESTED_JSON_DEPTH) return undefined
5552
try {
5653
return flattenJsonDiagnostic(JSON.parse(value), depth + 1)
@@ -80,6 +77,7 @@ function flattenJsonDiagnostic(value: unknown, depth = 0): string | undefined {
8077

8178
function decodeNestedJsonDiagnostic(value: string): string | undefined {
8279
if (value.length > MAX_OCI_ERROR_INPUT_LENGTH) return undefined
80+
if (containsEmbeddedStructuredText(value)) return undefined
8381
if (!looksLikeStructuredJson(value)) return value
8482
try {
8583
return flattenJsonDiagnostic(JSON.parse(value))
@@ -94,7 +92,8 @@ function sanitizeOciErrorField(
9492
): string | undefined {
9593
if (typeof value !== 'string') return undefined
9694
if (value.length > MAX_OCI_ERROR_INPUT_LENGTH) return undefined
97-
if (containsEncodedDiagnosticSentinel(value)) return undefined
95+
if (/%[0-9a-f]{2}/i.test(value)) return undefined
96+
if (/\(request-target\)|x-content-sha256/i.test(value)) return undefined
9897
const decoded = decodeNestedJsonDiagnostic(value)
9998
if (decoded === undefined) return undefined
10099
const exactValues = sensitiveValues.flatMap((sensitiveValue) => {

0 commit comments

Comments
 (0)