Skip to content

Commit c6afd79

Browse files
committed
fix(jupyter): also reject percent-encoded traversal segments
A segment like %2e%2e wouldn't match the literal '..' check. Now decodes each segment before comparing, in addition to the literal check, so an already-encoded traversal attempt is caught too.
1 parent de41944 commit c6afd79

1 file changed

Lines changed: 21 additions & 2 deletions

File tree

apps/sim/tools/jupyter/utils.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,38 @@ 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+
6179
/**
6280
* Rejects `.` and `..` segments in a Jupyter contents path, which could
6381
* otherwise traverse outside the intended directory on the target server.
6482
* Shared by every helper that sends a path to Jupyter, whether in a URL or a
6583
* request body.
6684
*
67-
* @throws {UnsafeJupyterPathError} when a segment is `.` or `..`.
85+
* @throws {UnsafeJupyterPathError} when a segment is `.` or `..`, literally
86+
* or percent-encoded.
6887
*/
6988
function assertNoJupyterPathTraversal(path: string | undefined): string[] {
7089
const segments = (path ?? '').split('/').filter((segment) => segment.length > 0)
7190

7291
for (const segment of segments) {
73-
if (segment === '.' || segment === '..') {
92+
if (isTraversalSegment(segment)) {
7493
throw new UnsafeJupyterPathError(path ?? '')
7594
}
7695
}

0 commit comments

Comments
 (0)