Skip to content

fix: encode caller-supplied path parameters in legacy modules#1662

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
feature/sec-1216-path-parameter-injection-in-workos-node-legacy-modules-user
Open

fix: encode caller-supplied path parameters in legacy modules#1662
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
feature/sec-1216-path-parameter-injection-in-workos-node-legacy-modules-user

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Description

⚠️ Automated first-pass security fix from triage of SEC-1216. Requires human security review before merge. Not approved/merged by automation.

Fixes a path-parameter injection in the hand-written "legacy" SDK modules (User Management, MFA, SSO, Organizations, Organization Domains, Directory Sync, Authorization/FGA, API Keys, Feature Flags, Audit Logs, Passwordless).

Vulnerability. These modules interpolate caller-supplied identifiers straight into URL path templates without encoding:

async deleteFactor(id: string) {
  await this.workos.delete(`/auth/factors/${id}`);   // id is raw
}

HttpClient.getResourceURL then resolves the path with the WHATWG URL parser (new URL(path, baseURL)), which honors ../ dot-segments and ?/# metacharacters. So an integrator that forwards an attacker-influenced identifier into one of these methods can have its API-key-authenticated request retargeted to an arbitrary same-verb WorkOS endpoint. Observed today (fetch intercepted, key redacted):

deleteFactor("../../user_management/users/user_01VICTIM")
  -> DELETE https://api.workos.com/user_management/users/user_01VICTIM   [Bearer sk_...]
acceptInvitation("../organization_memberships/om_X/reactivate?")
  -> POST   .../organization_memberships/om_X/reactivate?/accept        (? neutralizes /accept)

The auto-generated modules (webhooks, vault, ...) are unaffected because they already wrap parameters in encodeURIComponent.

Fix. Wrap every interpolated path parameter in the legacy modules with a shared encodePathParameter helper (src/common/utils/encode-path-parameter.ts), matching the encoding pattern already used by the auto-generated modules. After the fix the same payloads stay within one path segment:

deleteFactor("../../user_management/users/user_01VICTIM")
  -> DELETE https://api.workos.com/auth/factors/..%2F..%2Fuser_management%2Fusers%2Fuser_01VICTIM

The helper is encodeURIComponent with one deviation: a literal : is preserved. Colons are valid path-segment characters and are used by WorkOS RBAC slugs (e.g. users:read); the interpolated value is always preceded by a /-delimited segment, so a : can never be read as a URL scheme. This keeps the wire format identical for existing slugs (no behavior change for legitimate identifiers — encodePathParameter("user_01ABC") === "user_01ABC").

Scope: 99 interpolation sites across 11 legacy modules; each interpolated value is a single identifier/slug/token path segment. No auto-generated files changed.

Testing

  • Added encode-path-parameter.spec.ts (identity for normal IDs, : preserved, / ? # and .. encoded).
  • npm test (906 tests), npm run lint, npm run prettier, tsc --noEmit all pass.
  • Existing URL-asserting specs pass unchanged (encoding is a no-op for the normal IDs/slugs they use).

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] No

Link to Devin session: https://app.devin.ai/sessions/77c068d1a6b94f87bb7779fd80c111a9

The hand-written legacy modules interpolate caller-supplied identifiers
(user IDs, invitation tokens, MFA factor IDs, organization IDs, external
IDs, role/permission slugs) directly into URL path templates without
encoding. HttpClient.getResourceURL then resolves the result with the
WHATWG URL parser, which honors ../ dot-segments and ?/# metacharacters.
An integrator that forwards an attacker-influenced identifier into one of
these methods could have its API-key-authenticated request retargeted to
an arbitrary same-verb WorkOS endpoint.

Wrap each interpolated path parameter in a shared encodePathParameter
helper, matching the encodeURIComponent pattern already used by the
auto-generated modules. The helper preserves ':' so existing RBAC slugs
(e.g. users:read) keep their wire format.
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author
Original prompt from Linear User

Please work on ticket "Path parameter injection in workos-node legacy modules (User Management, MFA, SSO, Organizations, Directory Sync, Authorization/FGA, API Keys, Feature Flags, Audit Logs) redirects API-key-authenticated requests to arbitrary WorkOS API endpoints" (SEC-1216)

@playbook:playbook-b588614117c7477a9b9729928385384f

@devin-ai-integration
devin-ai-integration Bot requested review from a team as code owners July 21, 2026 19:50
@devin-ai-integration
devin-ai-integration Bot requested a review from csrbarber July 21, 2026 19:50
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@linear-code

linear-code Bot commented Jul 21, 2026

Copy link
Copy Markdown

SEC-1216

@devin-ai-integration devin-ai-integration Bot changed the title Encode caller-supplied path parameters in legacy modules fix: encode caller-supplied path parameters in legacy modules Jul 21, 2026
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents caller-supplied identifiers from changing legacy SDK request paths. The main changes are:

  • Adds a shared path-parameter encoding helper.
  • Rejects bare . and .. path segments.
  • Applies encoding across 11 legacy SDK modules.
  • Adds focused tests for traversal payloads and valid identifiers.

Confidence Score: 5/5

This looks safe to merge.

  • The bare dot-segment escape is blocked before URL construction.
  • Encoded traversal payloads remain inside one path segment.
  • No blocking issue was found in the updated code.

Important Files Changed

Filename Overview
src/common/utils/encode-path-parameter.ts Adds centralized path-segment encoding and rejects bare dot segments before URL parsing.
src/common/utils/encode-path-parameter.spec.ts Tests ordinary identifiers, colon-bearing slugs, URL metacharacters, encoded traversal input, and dot-only segments.
src/authorization/authorization.ts Encodes identifiers and slugs in authorization request paths.
src/user-management/user-management.ts Encodes user, membership, invitation, verification, and session path parameters.
src/feature-flags/feature-flags.ts Encodes feature flag and target parameters, including non-terminal route segments.

Reviews (2): Last reviewed commit: "Reject dot-only path segments in encodeP..." | Re-trigger Greptile

Comment thread src/common/utils/encode-path-parameter.ts Outdated
encodeURIComponent leaves '.' and '..' unchanged, and the WHATWG URL
parser removes them as relative path segments. For a non-terminal
template such as /feature-flags/${slug}/enable a slug of '..' still
climbs and retargets the request (e.g. to /enable). Percent-encoding the
dots does not help because the parser also treats the %2e forms as dot
segments. Fail closed: throw for '.' and '..', which are never valid
WorkOS identifiers.
@csrbarber
csrbarber requested a review from gjtorikian July 22, 2026 16: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.

0 participants