Skip to content

Commit 280ad02

Browse files
committed
fix(jupyter): decode the whole path before splitting, not per-already-split segment
A segment like foo%2f..%2fsecret has no literal slash, so splitting on literal '/' first and decoding each piece in isolation treats it as one opaque segment and never notices the '..' hiding behind the encoded slash. Decode the full path once, then split and check every segment the target server's own single URL-decode pass would see.
1 parent fdd45a6 commit 280ad02

1 file changed

Lines changed: 19 additions & 25 deletions

File tree

apps/sim/tools/jupyter/utils.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -58,43 +58,37 @@ export class UnsafeJupyterPathError extends Error {
5858
}
5959
}
6060

61-
/**
62-
* Whether a path segment is `.`/`..`, checked both literally and after
63-
* percent-decoding — a caller could submit an already-encoded `%2e%2e` to try
64-
* to slip a traversal segment past a literal-only check.
65-
*/
66-
function isTraversalSegment(segment: string): boolean {
67-
if (segment === '.' || segment === '..') return true
68-
69-
let decoded: string
70-
try {
71-
decoded = decodeURIComponent(segment)
72-
} catch {
73-
return false
74-
}
75-
76-
return decoded === '.' || decoded === '..'
77-
}
78-
7961
/**
8062
* Rejects `.` and `..` segments in a Jupyter contents path, which could
8163
* otherwise traverse outside the intended directory on the target server.
8264
* Shared by every helper that sends a path to Jupyter, whether in a URL or a
8365
* request body.
8466
*
67+
* Decodes the *entire* path once before splitting it, rather than splitting
68+
* on literal `/` first and decoding each piece in isolation — a segment like
69+
* `foo%2f..%2fsecret` has no literal slash, so a split-then-decode check
70+
* would treat it as one opaque segment and never notice the `..` hiding
71+
* behind the encoded slash. Decoding first exposes every segment the target
72+
* server would actually see once its own single URL-decode pass runs.
73+
*
8574
* @throws {UnsafeJupyterPathError} when a segment is `.` or `..`, literally
86-
* or percent-encoded.
75+
* or percent-encoded (including an encoded slash exposing a hidden segment).
8776
*/
8877
function assertNoJupyterPathTraversal(path: string | undefined): string[] {
89-
const segments = (path ?? '').split('/').filter((segment) => segment.length > 0)
78+
const raw = path ?? ''
79+
80+
let decoded: string
81+
try {
82+
decoded = decodeURIComponent(raw)
83+
} catch {
84+
decoded = raw
85+
}
9086

91-
for (const segment of segments) {
92-
if (isTraversalSegment(segment)) {
93-
throw new UnsafeJupyterPathError(path ?? '')
94-
}
87+
if (decoded.split('/').some((segment) => segment === '.' || segment === '..')) {
88+
throw new UnsafeJupyterPathError(raw)
9589
}
9690

97-
return segments
91+
return raw.split('/').filter((segment) => segment.length > 0)
9892
}
9993

10094
/**

0 commit comments

Comments
 (0)