Skip to content

Commit 3fa59e7

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(oci): preserve encoded resource path separators
1 parent 1b74526 commit 3fa59e7

2 files changed

Lines changed: 70 additions & 3 deletions

File tree

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

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,60 @@ describe('credential-bound OCI client', () => {
372372
})
373373
})
374374

375+
it.each([
376+
'reports%2Fdaily.csv',
377+
'reports%2fdaily%5cfile.csv',
378+
'%2Freports%2F%2Fdaily.csv',
379+
'%2F',
380+
'%5Creports%5Cdaily.csv',
381+
'reports%2F..%2Fdaily.csv',
382+
'reports%2F%E2%98%83%20caf%C3%A9.csv',
383+
'literal%252F%255C%2500.csv',
384+
'what%3Fpart%231.txt',
385+
])('preserves and signs an encoded object name exactly: %s', async (encodedName) => {
386+
const { client } = await createPreparedClient()
387+
const endpoint = await client.prepareStaticEndpoint(
388+
createOciStaticEndpointPolicy({
389+
serviceId: OCI_SERVICE_ID,
390+
serviceName: 'objectstorage',
391+
hostnameTemplate: 'regional',
392+
})
393+
)
394+
const encodedPath = `/n/synthetic_namespace/b/synthetic_bucket/o/${encodedName}`
395+
const target = `${encodedPath}?versionId=v%2F1`
396+
await client.request({
397+
endpoint,
398+
method: 'GET',
399+
encodedPath,
400+
queryPairs: [['versionId', 'v/1']],
401+
timeoutMs: 10_000,
402+
maxResponseBytes: 1024,
403+
})
404+
expect(mocks.secureFetch).toHaveBeenCalledOnce()
405+
const [url, , options] = mocks.secureFetch.mock.calls[0] as [
406+
string,
407+
string,
408+
{ headers: Record<string, string> },
409+
]
410+
const hostname = 'objectstorage.us-ashburn-1.oraclecloud.com'
411+
expect(url).toBe(`https://${hostname}${target}`)
412+
const signature = options.headers.authorization.match(/signature="([^"]+)"/)?.[1]
413+
expect(signature).toBeDefined()
414+
const signingString = [
415+
`x-date: ${options.headers['x-date']}`,
416+
`(request-target): get ${target}`,
417+
`host: ${hostname}`,
418+
].join('\n')
419+
expect(
420+
verify(
421+
'RSA-SHA256',
422+
Buffer.from(signingString, 'utf8'),
423+
createPublicKey(PRIVATE_KEY),
424+
Buffer.from(signature ?? '', 'base64')
425+
)
426+
).toBe(true)
427+
})
428+
375429
it.each(['GET', 'HEAD', 'DELETE'] as const)('rejects bodies for %s', async (method) => {
376430
const { client, endpoint } = await createPreparedClient()
377431
await expect(
@@ -437,13 +491,23 @@ describe('credential-bound OCI client', () => {
437491
'//host/path',
438492
'/double//slash',
439493
'/query?x=1',
494+
'/fragment#value',
440495
'/back\\slash',
441-
'/encoded%2Fslash',
442-
'/encoded%5Cbackslash',
443496
'/encoded%00control',
444497
'/encoded%1fcontrol',
445498
'/encoded%7Fcontrol',
446499
'/bad%2',
500+
'/bad%GG',
501+
'/raw\0control',
502+
'/raw\u007fcontrol',
503+
'/raw\ncontrol',
504+
'/raw\tcontrol',
505+
'/raw space',
506+
'/raw\ud800surrogate',
507+
'/a/../b',
508+
'/a/./b',
509+
'/a/%2e%2e/b',
510+
'/a/%2E/b',
447511
])('rejects ambiguous encoded paths: %s', async (encodedPath) => {
448512
const { client, endpoint } = await createPreparedClient()
449513
await expect(
@@ -455,6 +519,8 @@ describe('credential-bound OCI client', () => {
455519
maxResponseBytes: 1024,
456520
})
457521
).rejects.toMatchObject({ code: 'invalid_request' })
522+
expect(mocks.validateUrl).not.toHaveBeenCalled()
523+
expect(mocks.secureFetch).not.toHaveBeenCalled()
458524
})
459525

460526
it('rejects signing-controlled headers', async () => {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export type OciRetryPolicy = OciSafeRetryPolicy | OciTokenizedRetryPolicy
6262

6363
interface OciRequestBase {
6464
readonly endpoint: OciPreparedEndpoint
65+
/** Exact encoded path; encode each raw parameter once, including any embedded separators. */
6566
readonly encodedPath: string
6667
readonly queryPairs?: readonly (readonly [string, string])[]
6768
readonly headers?: Readonly<Record<string, string>>
@@ -418,7 +419,7 @@ function buildRequestUrl(
418419
encodedPath.startsWith('//') ||
419420
encodedPath.includes('//') ||
420421
/[?#\\\u0000-\u001f\u007f]/.test(encodedPath) ||
421-
/%(?:0[0-9a-f]|1[0-9a-f]|2f|5c|7f)/i.test(encodedPath) ||
422+
/%(?:0[0-9a-f]|1[0-9a-f]|7f)/i.test(encodedPath) ||
422423
/%(?![0-9a-f]{2})/i.test(encodedPath)
423424
) {
424425
throw new OciClientError('invalid_request')

0 commit comments

Comments
 (0)