fix(dynamodb-mcp-server): validate and escape schema names in Python generator - #4579
Open
LeeroyHannigan wants to merge 5 commits into
Open
fix(dynamodb-mcp-server): validate and escape schema names in Python generator#4579LeeroyHannigan wants to merge 5 commits into
LeeroyHannigan wants to merge 5 commits into
Conversation
…generator The Python data-access-layer generator renders source code through Jinja2 with autoescaping disabled, and nothing downstream escaped schema values, so values from schema.json were written into the generated code verbatim. Schema validation did not prevent this: it checked required fields, JSON types, enum values and uniqueness, but applied no character or identifier constraint to any name, while a comment in the generator asserted the opposite. This applies to the Python generator the discipline #4384 applied to the CDK generator. Values are constrained during schema validation, which already gates rendering, so an invalid schema is rejected before any template runs. A new core/name_validator.py holds the rules, applied according to where a value lands in the generated code: - Identifier positions -- entity names, field names, access pattern names, parameter names and key template placeholders become class names, def names and attribute names, where escaping is not possible. These must be valid non-keyword Python identifiers. In practice this is not a new restriction: a value that is not an identifier produced a SyntaxError, not usable code. - Identifier fragments -- a GSI name becomes a suffix of generated method names, so it needs only identifier characters and may begin with a digit. - String-literal positions -- table names, key attribute names, entity types and index names are checked only for characters that would end the enclosing literal, docstring or comment, and for braces, because some of these positions are f-strings where a brace is an expression. DynamoDB permits almost any character in an attribute name, so spaces and non-ASCII text remain valid. - Prose -- descriptions additionally allow apostrophes, which are ordinary in English. Key templates are validated separately: they are rendered into f-strings, so a brace region that is not a supported {placeholder} is rejected. The existing placeholder regex matches only word-character placeholders and ignores any other brace region, so such a region previously reached an f-string unchecked. Covered sinks that no validator previously visited: parameter defaults (rendered into a def signature, and now also escaped with json.dumps in the generator so correctness does not rest on validation alone), parameter descriptions, filter expression parameter references, cross-table pattern names and descriptions, and entity involvement conditions. Table-level gsi_list entries are covered too -- GSIValidator validated their structure but never their characters, while their key attribute names reach generated query code. Separately, the usage_data string escaper handled backslashes and double quotes but not newlines, so a newline in a sample value produced an unterminated string literal in the generated example. It now escapes newlines and carriage returns as well. Left as-is, having been confirmed already constrained: range_condition and projection_type are restricted to enum allowlists, item_type is never interpolated and unknown values map to Any, and parameter entity_type and entity involvement entity are membership-checked against entity names. Behaviour change: a schema whose entity or field names are not valid Python identifiers is now rejected with a clear validation error instead of producing code that cannot be imported. One property test asserted that arbitrary text is a valid GSI name; its strategy is narrowed to names usable in generated code, and the new tests cover rejection.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4579 +/- ##
========================================
Coverage 93.27% 93.27%
========================================
Files 1053 1054 +1
Lines 89208 89344 +136
Branches 14385 14441 +56
========================================
+ Hits 83205 83337 +132
+ Misses 3626 3624 -2
- Partials 2377 2383 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Adds coverage for the branch in each validator that reports a non-string value instead of raising, and marks the defensive isinstance re-check in validate_key_template as unreachable through that function, since validate_literal_safe rejects a non-string first.
…ibutes Covers the remaining validation branches for table-level gsi_list entries: composite partition and sort keys given as attribute lists, included_attributes, and a malformed entry that the name checks must walk past without raising.
Moves the string type check in validate_key_template ahead of the delegation to validate_literal_safe. The check was previously placed after it, which made it unreachable and required a coverage exclusion; doing it first also makes the function's own type narrowing explicit rather than inherited.
willykev
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Python data-access-layer generator (
repo_generation_tool) renders source code throughJinja2 with autoescaping disabled, and nothing downstream escaped schema values, so values
from
schema.jsonwere written into the generated code verbatim.Schema validation did not prevent this. It checked required fields, JSON types, enum values
and uniqueness, but applied no character or identifier constraint to any name — while a
comment in
jinja2_generator.pyasserted the opposite ("Schema validation ensures all inputsare safe before template rendering"). That comment is corrected here.
This applies to the Python generator the same discipline #4384 applied to the CDK generator.
Values are constrained during schema validation, which already gates rendering (
generate()returns before
create_generator()when validation fails), so an invalid schema is rejectedbefore any template runs.
Approach
A new
core/name_validator.pyholds the rules, applied according to where a value lands inthe generated code — the position determines what is possible, so one rule does not fit all:
Escaping is impossible in an identifier position, which is why those values must be rejected
rather than escaped. Conversely, DynamoDB permits almost any character in an attribute name,
so string-literal positions are not restricted to an identifier — spaces and non-ASCII text
remain valid.
Braces are rejected in string-literal positions because several of those positions are
f-strings, where a brace is a live expression rather than literal text.
Key templates are validated separately: they are rendered into f-strings, so a brace region
that is not a supported
{placeholder}is rejected. The existing placeholder regex matchesonly word-character placeholders and silently ignores any other brace region, so such a region
previously reached an f-string unchecked.
Sinks covered
Beyond the names above, these reached generated code and no validator visited them:
default— rendered into a generateddefsignature. Validated here, and thegenerator now also escapes it with
json.dumpsso correctness does not rest on validationalone.
description— reaches the generated method docstring.filter_expressionparam/param2/params[]— reach a docstring and comments.nameanddescription— reach adefname and its docstring in thetransaction service.
entities_involved[].condition— reaches comments in the transaction service.gsi_listentries —GSIValidatorvalidated their structure but never theircharacters, while their key attribute names reach generated query code.
Separately, the
usage_datastring escaper handled backslashes and double quotes but notnewlines, so a newline in a sample value produced an unterminated string literal in the
generated example. It now escapes newlines and carriage returns too.
Confirmed already constrained and deliberately left alone:
range_conditionandprojection_typeare restricted to enum allowlists;item_typeis never interpolated andunknown values map to
Any; parameterentity_typeandentities_involved[].entityaremembership-checked against entity names, which this change makes safe.
Behaviour change
A schema whose entity or field names are not valid Python identifiers is now rejected with a
clear validation error instead of producing code that cannot be imported. Worth a release note.
Testing
tests/repo_generation_tool/unit/test_name_validator.py: unit coverage per rule plusend-to-end assertions that a schema targeting each sink fails validation and writes no file.
the two that produced executable output, executability was confirmed beforehand by parsing
the generated file and finding the payload as a live AST node rather than as text in a
comment — and after the change both are rejected during validation.
and a description containing an apostrophe (36 of the 447 descriptions in the repo fixtures
contain apostrophes, which is why prose allows them).
ruff check,ruff format --checkandpyrightclean.pre-commitcould not run end to end in my environment: itsgitleakshook cannot fetch Gomodules through my network. The Python hooks it wraps (
ruff,pyright) were run directly.One pre-existing property test asserted that arbitrary text is a valid GSI name, which encoded
the unsafe behaviour; its strategy is narrowed to names usable in generated code, with
rejection covered by the new tests.
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.