Skip to content

fix: url-encode path parameters in php/node/python/kotlin emitters#75

Merged
gjtorikian merged 1 commit intomainfrom
fix-path-traversal-encoding
May 2, 2026
Merged

fix: url-encode path parameters in php/node/python/kotlin emitters#75
gjtorikian merged 1 commit intomainfrom
fix-path-traversal-encoding

Conversation

@gjtorikian
Copy link
Copy Markdown
Collaborator

@gjtorikian gjtorikian commented May 1, 2026

Summary

Closes a path-traversal / cross-resource-API-call vulnerability in the php, node, python, and kotlin emitters.

The four affected emitters were interpolating caller-supplied ids directly into request paths without per-segment URL-encoding. A value like ../webhook_endpoints/wh_target was silently normalized by libcurl (RFC 3986 dot-segment removal) before transmission, so the SDK request reached a different endpoint of the WorkOS API while still authenticated with the application's bearer token — i.e. forged cross-resource API requests under the application's API key.

Approach

  • New shared parser src/shared/path-template.ts (with 11 unit tests): splits an OpenAPI path template like /orgs/{id}/users/{uid} into ordered literal/param segments. Now used by all four affected emitters instead of each rolling its own regex.
  • Per-language renderers (one short file per language) under src/{php,node,python,kotlin}/path-expression.ts. Rendering stays per-language because the grammars differ enough — PHP can't call functions inside "..." so it uses concatenation; Node uses template literals; Python uses f-strings with urllib.parse.quote(..., safe=""); Kotlin uses string templates with a runtime helper.
  • Removed dead buildPathExpression / buildPathExpr / buildPathStr duplicates from each emitter.
Lang Before After
php "connections/{$id}" 'connections/' . rawurlencode($id)
node `connections/${id}` `connections/${encodeURIComponent(id)}`
python f"connections/{id}" f"connections/{quote(str(id), safe='')}" (+ auto-injected from urllib.parse import quote)
kotlin "connections/${id}" "connections/${encodePathSegment(id)}" (+ auto-imported com.workos.common.http.encodePathSegment)

Languages already safe (no changes)

  • dotnet — Uri.EscapeDataString
  • go — url.PathEscape
  • ruby — WorkOS::Util.encode_path

Kotlin runtime dependency — outstanding

The kotlin emitter now imports com.workos.common.http.encodePathSegment, which does not yet exist in workos-kotlin. This is not blocking:

  • The current workos-kotlin/main branch is structurally older than what the emitter generates (different RequestConfig shape, different baseClient.request(...) signature). Kotlin regen is gated on the broader generator-cutover work anyway.
  • When that cutover happens, whoever lands it must also add the runtime helper. Suggested implementation:
// src/main/kotlin/com/workos/common/http/UrlEncoding.kt
package com.workos.common.http

import java.net.URLEncoder

fun encodePathSegment(value: String): String {
  return URLEncoder.encode(value, Charsets.UTF_8)
    .replace("+", "%20")
    .replace("*", "%2A")
    .replace("%7E", "~")
}

URLEncoder.encode is form-encoding (encodes space as +, leaves a few path-reserved chars), so the post-processing is required for RFC 3986 path-segment correctness.

Companion PR

  • workos-php #379: defense-in-depth runtime guard in HttpClient::resolveUrl() that rejects .. / ? / # / CRLF. Lands independently of regen so existing PHP SDK users get protection immediately, and stays valuable after this emitter PR ships.

Versioning

After regen, php / node / python release as patch (security fix). No public API surface changes — only the wire-format path is fixed. Kotlin regen is gated on the unrelated generator-cutover work.

Test plan

  • npx vitest run — 399 passed (45 files), 11 new tests in test/shared/path-template.test.ts
  • npx tsc --noEmit — clean
  • npx oxlint -D warnings — 0 warnings, 0 errors
  • npx oxfmt --check . — clean
  • Regenerate sdk-php / sdk-node / sdk-python, smoke-test against live API
  • Visual diff of generated path strings to confirm encoding

🤖 Generated with Claude Code

The php, node, python, and kotlin emitters were interpolating caller-supplied
ids directly into request paths without per-segment URL-encoding. A value
like "../webhook_endpoints/wh_target" was silently normalized by libcurl
(RFC 3986 dot-segment removal) before transmission, so the request reached a
different endpoint of the WorkOS API while still authenticated with the
application's bearer token — i.e. forged cross-resource API requests under
the application's API key.

Add a shared `parsePathTemplate` helper in `src/shared/path-template.ts` that
splits an OpenAPI path template into ordered literal/param segments. Each of
the four affected emitters now renders parameters through a per-language
URL-encoding call:

- php: `'orgs/' . rawurlencode($id) . '/foo'` (concatenation; PHP can't call
  functions inside `"..."`)
- node: `` `orgs/${encodeURIComponent(id)}/foo` ``
- python: `f"orgs/{quote(str(id), safe='')}/foo"` (with auto-injected
  `from urllib.parse import quote` import)
- kotlin: `"orgs/${encodePathSegment(id)}/foo"` (with auto-imported runtime
  helper `com.workos.common.http.encodePathSegment` — the helper is added in
  a separate workos-kotlin PR)

dotnet, go, and ruby were already encoding (Uri.EscapeDataString,
url.PathEscape, WorkOS::Util.encode_path) so no changes there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gjtorikian gjtorikian merged commit 34c08fc into main May 2, 2026
6 checks passed
@gjtorikian gjtorikian deleted the fix-path-traversal-encoding branch May 2, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant