Skip to content

Commit c27da33

Browse files
committed
fix(jupyter): reject path-traversal segments in Jupyter content paths
encodeJupyterPath now rejects '.'/'..' segments across the whole path (shared by all 16 tools, not just upload); the upload route returns a clean 400 when it's hit.
1 parent d1d326a commit c27da33

2 files changed

Lines changed: 35 additions & 6 deletions

File tree

apps/sim/app/api/tools/jupyter/upload/route.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
buildJupyterAuthHeaders,
1919
encodeJupyterPath,
2020
normalizeJupyterServerUrl,
21+
UnsafeJupyterPathError,
2122
} from '@/tools/jupyter/utils'
2223

2324
export const dynamic = 'force-dynamic'
@@ -76,7 +77,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
7677
const base = normalizeJupyterServerUrl(data.serverUrl)
7778
const destinationDirectory = (data.directory ?? '').replace(/\/+$/, '')
7879
const destinationPath = destinationDirectory ? `${destinationDirectory}/${fileName}` : fileName
79-
const uploadUrl = `${base}/api/contents/${encodeJupyterPath(destinationPath)}`
80+
81+
let encodedDestinationPath: string
82+
try {
83+
encodedDestinationPath = encodeJupyterPath(destinationPath)
84+
} catch (error) {
85+
if (error instanceof UnsafeJupyterPathError) {
86+
return NextResponse.json({ success: false, error: error.message }, { status: 400 })
87+
}
88+
throw error
89+
}
90+
const uploadUrl = `${base}/api/contents/${encodedDestinationPath}`
8091

8192
const urlValidation = await validateUrlWithDNS(uploadUrl, 'serverUrl', { allowHttp: true })
8293
if (!urlValidation.isValid || !urlValidation.resolvedIP) {

apps/sim/tools/jupyter/utils.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,34 @@ export function buildJupyterAuthHeaders(token: string): Record<string, string> {
4747
return { Authorization: `token ${token}` }
4848
}
4949

50+
/**
51+
* Error thrown when a user-supplied Jupyter contents path contains a `.` or
52+
* `..` segment that could traverse outside the intended directory.
53+
*/
54+
export class UnsafeJupyterPathError extends Error {
55+
constructor(rawPath: string) {
56+
super(`Invalid Jupyter path: ${rawPath}`)
57+
this.name = 'UnsafeJupyterPathError'
58+
}
59+
}
60+
5061
/**
5162
* Encodes a Jupyter contents path segment-by-segment so slashes stay as path
5263
* separators while special characters within a segment are escaped.
64+
*
65+
* @throws {UnsafeJupyterPathError} when a segment is `.` or `..`, which could
66+
* otherwise traverse outside the intended directory on the target server.
5367
*/
5468
export function encodeJupyterPath(path: string | undefined): string {
55-
return (path ?? '')
56-
.split('/')
57-
.filter((segment) => segment.length > 0)
58-
.map(encodeURIComponent)
59-
.join('/')
69+
const segments = (path ?? '').split('/').filter((segment) => segment.length > 0)
70+
71+
for (const segment of segments) {
72+
if (segment === '.' || segment === '..') {
73+
throw new UnsafeJupyterPathError(path ?? '')
74+
}
75+
}
76+
77+
return segments.map(encodeURIComponent).join('/')
6078
}
6179

6280
interface RawJupyterKernel {

0 commit comments

Comments
 (0)