Files
responder/tests/test_clientgen_pathparam_escaping.py
kennethreitz c839db5eac Escape path-param names in generated client string literals (#618)
## Summary

Follow-up hardening from the fresh audit. The client generator
interpolated OpenAPI path-parameter names directly into single-quoted
string literals in the generated Python/JavaScript/Ruby clients:

```python
path_expr += f".replace('{{{raw_name}}}', _quote({py_name}))"   # Python, before
```

A path-param name containing a `'` or `\` — reachable from an
**untrusted** OpenAPI spec (the path-param regex `{([^}:]+)...}` allows
both) — breaks out of the literal and produces a client that fails to
parse. Impact is limited to broken *generated* output (not generator
RCE), but it's a real correctness bug for third-party specs.

## Fix

Emit the `{name}` placeholder through a proper per-language string
escaper:
- **Python** → `repr(...)`
- **JavaScript/TypeScript** → the existing `_js_string(...)`
- **Ruby** → a new `_ruby_string(...)` (single-quoted literal; also
applied to the Ruby base path and query-hash keys, which had the same
`repr`-for-Ruby quirk where a value with a quote could force a
double-quoted literal and re-enable `#{}` interpolation)

**PHP was already correct** and is unchanged — `_php_string` escapes `\`
and `'`, and single-quoted PHP tolerates literal newlines (so the
audit's PHP "newline" finding was a false positive).

## Tests

New `tests/test_clientgen_pathparam_escaping.py`: feeds a hostile spec
(path-param name `a'b\c\n d`) and asserts the generated **Python
compiles** (`compile()` — would `SyntaxError` before), and that the
JS/Ruby placeholders are emitted through the escaper. The existing
multi-language native syntax checks (`test_clientgen.py`) still pass.

Full suite: **759 passed**, 1 skipped (php toolchain not installed
locally; runs in CI); ruff clean; mypy clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:50:28 -04:00

64 lines
2.2 KiB
Python

"""Path-parameter names from an (untrusted) OpenAPI spec must be escaped when
interpolated into generated client string literals.
A name containing a quote/backslash/newline previously broke out of the
single-quoted `.replace('{name}', ...)` / `.gsub(...)` literal, producing a
client that fails to parse. We feed a hostile spec and check the output is
well-formed.
"""
from responder.ext.clientgen import _js_string, _ruby_string, generate_client
# A path-parameter name loaded with characters that break naive string
# interpolation: single quote, backslash, and a newline.
NASTY = "a'b\\c\nd"
def _spec():
return {
"openapi": "3.0.2",
"info": {"title": "T", "version": "1"},
"paths": {
"/x/{" + NASTY + "}": {
"get": {
"operationId": "get_x",
"parameters": [
{
"name": NASTY,
"in": "path",
"required": True,
"schema": {"type": "string"},
}
],
"responses": {"200": {"description": "ok"}},
}
}
},
}
def test_python_client_compiles_with_hostile_path_param():
src = generate_client(_spec(), language="python")
# Would raise SyntaxError before the fix (unescaped quote/newline in literal).
compile(src, "<generated>", "exec")
def test_javascript_placeholder_is_escaped():
src = generate_client(_spec(), language="javascript")
# The placeholder is emitted through the JS string escaper, so the quote
# can't break out of the literal in the generated `.replace(...)` call.
placeholder = _js_string("{" + NASTY + "}")
assert f".replace({placeholder}, quote(" in src
def test_ruby_placeholder_is_escaped():
src = generate_client(_spec(), language="ruby")
placeholder = _ruby_string("{" + NASTY + "}")
assert f".gsub({placeholder}, quote(" in src
def test_typescript_client_still_generates():
# TS shares the JS emitter; just ensure it doesn't blow up.
src = generate_client(_spec(), language="typescript")
assert "get_x" in src or "getX" in src