Skip to content

Commit d959172

Browse files
committed
fix(http): close double-encoding bypass in path traversal guard
Replace single rawurldecode() with a decode-until-stable loop so that double-encoded sequences like %252e%252e cannot slip past the assertSafePath check. Add test cases for double-encoded traversal, slash, query character, and null byte variants.
1 parent f0343b5 commit d959172

2 files changed

Lines changed: 22 additions & 3 deletions

File tree

lib/HttpClient.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,13 @@ private function resolveUrl(string $path, ?RequestOptions $options): string
251251
* in a single ID would silently re-target the request at a different
252252
* WorkOS resource under the application's authenticated API key.
253253
*
254-
* The check runs against the percent-decoded path so that encoded
255-
* variants (`%2e%2e`, `%2f`, `%3f`, `%0d%0a`, ...) cannot bypass it.
254+
* The check runs against the fully percent-decoded path so that encoded
255+
* variants (`%2e%2e`, `%2f`, `%3f`, `%0d%0a`, ...) and double-encoded
256+
* variants (`%252e%252e`, `%252f`, ...) cannot bypass it.
256257
*/
257258
private function assertSafePath(string $path): void
258259
{
259-
$decoded = rawurldecode($path);
260+
$decoded = self::decodeUntilStable($path);
260261

261262
if (preg_match('/[\x00-\x1f?#]/', $decoded) === 1) {
262263
throw new \InvalidArgumentException(
@@ -273,6 +274,20 @@ private function assertSafePath(string $path): void
273274
}
274275
}
275276

277+
/**
278+
* Decode percent-encoded characters in a loop until the value stabilizes,
279+
* closing double-encoding bypass vectors like `%252e%252e`.
280+
*/
281+
private static function decodeUntilStable(string $value): string
282+
{
283+
do {
284+
$prev = $value;
285+
$value = rawurldecode($value);
286+
} while ($value !== $prev);
287+
288+
return $value;
289+
}
290+
276291
private function resolveTimeout(?RequestOptions $options): int
277292
{
278293
return $options !== null && $options->timeout !== null ? $options->timeout : $this->timeout;

tests/HttpClientTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ public static function unsafePathProvider(): array
108108
'percent-encoded fragment character' => ['connections/conn_123%23frag'],
109109
'percent-encoded CRLF injection' => ['connections/conn_123%0D%0AHost:%20evil'],
110110
'percent-encoded null byte' => ['connections/conn_123%00'],
111+
'double-encoded parent traversal' => ['connections/%252e%252e/webhook_endpoints'],
112+
'double-encoded slash hides traversal' => ['connections%252F..%252Fwebhook_endpoints'],
113+
'double-encoded query character' => ['connections/conn_123%253Foverride=1'],
114+
'double-encoded null byte' => ['connections/conn_123%2500'],
111115
];
112116
}
113117

0 commit comments

Comments
 (0)