Summary
During review of #3692 (security fix for unescaped OpenAPI strings in generated template literals), a related edge case was identified in packages/core/src/getters/route.ts's getRoutePath function: a literal $ immediately preceding a {param} in an OpenAPI path (e.g. /foo${petId}) is dropped in the generated template literal, producing /foo${petId} instead of /foo$${petId}.
Rationale (root cause, not just this instance)
The underlying problem is not this single edge case — it's the approach. Orval currently builds generated route/URL strings as raw JS template literals, and relies on ad-hoc heuristics (inspecting preceding characters such as \ vs $) to disambiguate a literal $ in the OpenAPI spec from the ${...} interpolation syntax it emits for path parameters. As more jsesc-based escaping of static segments is layered on top, these heuristics keep multiplying and are fragile by construction — every new escaping rule risks colliding with legitimate spec content in a new way.
Patching the heuristic for this one case (literal $ before {param}) does not address the underlying issue: any future OpenAPI path containing characters adjacent to ${/} sequences from jsesc escaping is a candidate for a similar bug.
Proposed fix
Move getRoutePath, getRoute, and getRouteAsArray (and any other generators building route/URL strings from spec content) away from constructing raw template-literal source text via character-adjacency heuristics. Instead, build the generated string using explicit string concatenation of separately-escaped literal segments and parameter placeholders (or an equivalent AST/token-based builder). This removes the need to guess intent from adjacent characters, since literal text and parameter interpolation are never emitted as raw overlapping template-literal syntax that has to be disambiguated after the fact.
Affected areas
packages/core/src/getters/route.ts (getRoutePath, getRoute, getRouteAsArray)
- Possibly other generators that build template-literal strings from OpenAPI spec content
Acceptance criteria
- Generated route/URL strings are built via string concatenation (or equivalent non-heuristic construction) instead of raw template-literal text with character-adjacency escaping heuristics.
- Literal
$ (and other characters that could collide with template-literal syntax) immediately preceding a {param} in an OpenAPI path is preserved correctly regardless of adjacency (e.g. /foo${petId} emits the correct literal $ before the parameter).
- No regression in generated output for existing test fixtures.
- Scoped for the v9 release per the maintainer's guidance.
References
Summary
During review of #3692 (security fix for unescaped OpenAPI strings in generated template literals), a related edge case was identified in
packages/core/src/getters/route.ts'sgetRoutePathfunction: a literal$immediately preceding a{param}in an OpenAPI path (e.g./foo${petId}) is dropped in the generated template literal, producing/foo${petId}instead of/foo$${petId}.Rationale (root cause, not just this instance)
The underlying problem is not this single edge case — it's the approach. Orval currently builds generated route/URL strings as raw JS template literals, and relies on ad-hoc heuristics (inspecting preceding characters such as
\vs$) to disambiguate a literal$in the OpenAPI spec from the${...}interpolation syntax it emits for path parameters. As more jsesc-based escaping of static segments is layered on top, these heuristics keep multiplying and are fragile by construction — every new escaping rule risks colliding with legitimate spec content in a new way.Patching the heuristic for this one case (literal
$before{param}) does not address the underlying issue: any future OpenAPI path containing characters adjacent to${/}sequences from jsesc escaping is a candidate for a similar bug.Proposed fix
Move
getRoutePath,getRoute, andgetRouteAsArray(and any other generators building route/URL strings from spec content) away from constructing raw template-literal source text via character-adjacency heuristics. Instead, build the generated string using explicit string concatenation of separately-escaped literal segments and parameter placeholders (or an equivalent AST/token-based builder). This removes the need to guess intent from adjacent characters, since literal text and parameter interpolation are never emitted as raw overlapping template-literal syntax that has to be disambiguated after the fact.Affected areas
packages/core/src/getters/route.ts(getRoutePath,getRoute,getRouteAsArray)Acceptance criteria
$(and other characters that could collide with template-literal syntax) immediately preceding a{param}in an OpenAPI path is preserved correctly regardless of adjacency (e.g./foo${petId}emits the correct literal $ before the parameter).References