Skip to content

Commit e0d676b

Browse files
committed
fix(files): don't reject external URLs containing '..' in file parse validation
The file block's file_fetch operation rejected any external URL whose path contained '..' (e.g. Slack files-pri slugs with a literal '...') with 'Access denied: path traversal detected'. Traversal checks only apply to local paths — external http(s) URLs are fetched with SSRF protection downstream and are never resolved against the filesystem, so they now short-circuit as valid. Internal /api/files/serve/ URLs keep full traversal protection.
1 parent 1ae1afb commit e0d676b

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

apps/sim/app/api/files/parse/route.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,39 @@ describe('Files Parse API - Path Traversal Security', () => {
796796
}
797797
})
798798

799+
it('should not treat .. inside external URLs as path traversal', async () => {
800+
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({
801+
isValid: true,
802+
resolvedIP: '203.0.113.10',
803+
})
804+
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValue(
805+
new Response('slack file content', {
806+
status: 200,
807+
headers: { 'content-type': 'text/plain' },
808+
})
809+
)
810+
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('write')
811+
812+
// Slack truncates long titles with a literal ellipsis, so the slug contains `..`
813+
const slackUrl =
814+
'https://files.slack.com/files-pri/T08-F0B/_other__no_invitation_messages_get_sent_-_sim_on_railway...txt'
815+
816+
const request = new NextRequest('http://localhost:3000/api/files/parse', {
817+
method: 'POST',
818+
body: JSON.stringify({ filePath: slackUrl, workspaceId: 'workspace-id' }),
819+
})
820+
821+
const response = await POST(request)
822+
const result = await response.json()
823+
824+
expect(result.error).not.toMatch(/Access denied: path traversal detected/)
825+
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).toHaveBeenCalledWith(
826+
slackUrl,
827+
'203.0.113.10',
828+
expect.any(Object)
829+
)
830+
})
831+
799832
it('should handle encoded path traversal attempts', async () => {
800833
const encodedMaliciousPaths = [
801834
'/api/files/serve/%2e%2e%2f%2e%2e%2fetc%2fpasswd', // ../../../etc/passwd

apps/sim/app/api/files/parse/route.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,13 +419,26 @@ function assertParsedContentWithinLimit(content: string, maxBytes?: number): str
419419
}
420420

421421
/**
422-
* Validate file path for security - prevents null byte injection and path traversal attacks
422+
* Validate file path for security - prevents null byte injection and path traversal attacks.
423+
*
424+
* External URLs (`http`/`https`) are fetched over HTTP — with SSRF protection applied
425+
* downstream in `fetchExternalUrlToWorkspace` (DNS resolution + private/reserved IP blocking)
426+
* — and are never resolved against the filesystem, so `..`/`~` are legal URL content and must
427+
* not be rejected. Providers such as Slack routinely emit slugs containing a literal `...`.
428+
*
429+
* Internal file URLs (`/api/files/serve/...`) ARE resolved to storage keys and filesystem
430+
* paths via `extractStorageKey`, so they keep full traversal protection — only the leading-`/`
431+
* "outside allowed directory" check is relaxed for them, since that prefix is expected.
423432
*/
424433
function validateFilePath(filePath: string): { isValid: boolean; error?: string } {
425434
if (filePath.includes('\0')) {
426435
return { isValid: false, error: 'Invalid path: null byte detected' }
427436
}
428437

438+
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
439+
return { isValid: true }
440+
}
441+
429442
if (filePath.includes('..')) {
430443
return { isValid: false, error: 'Access denied: path traversal detected' }
431444
}

0 commit comments

Comments
 (0)