Repository navigation
security: refuse a trailing newline in whole-string validators - #797
Merged
Merged
Conversation
Python's `$` also matches just before a final newline, so `re.match(r"^[a-z]+$", "orders\n")` succeeds and the caller goes on to use "orders\n". #788 closed this in providers/_sql_safety.py. The same `^...$` plus `.match` shape guarded 36 more validator patterns across fluid_build/, fed by contract YAML (a `|` block scalar ends in a newline), .env values, CLI flags, LLM tool calls and percent-encoded URL queries. 31 of those patterns now end in `\Z`. Two call sites use `fullmatch` instead: the catalog quoting chokepoint, which also covers the BigQuery patterns callers pass in, and the federation workspace id, whose error message prints the pattern. Seen through the CLI before the fix, `fluid generate iac` put `region = "eu-west-1\n"` on the AWS provider, and `branch = "main\n"` on a Databricks job from `?branch=main%0A`. 81 other `$`-ending patterns stay as they are. Their input is stripped first, or they are deny lists and detectors where `\Z` would loosen the check, or they are parsers whose captured groups are what is used. tests/security/test_trailing_newline_anchors.py pins each fixed site with a value and the same value plus "\n". All 39 cases fail on the parent commit. The fix follows Django's username validators (ticket #30257, `$` to `\Z`) and the MCP Python SDK's tool-name check (#3076, `fullmatch`).
With `\Z`, provider_block_for dropped a binding region of "eu-west-1\n" as no region code. That left the provider on the environment's region and skipped _guard_region_move. Pinning the stripped value is no better. Both sovereignty checkers (policy/sovereignty.py, providers/aws/util/sovereignty.py) compare the region as written, so "us-east-1\n" passes `deniedRegions: [us-east-1]` while the provider would be pinned to us-east-1. provider_block_for now raises UnsupportedBindingError (aws-region-whitespace) for a region code with whitespace around it. generate iac and apply render it as a typed CLI error before anything is applied, and fluid diff reports the inspector error. On main the value went onto the provider with its newline.
copilot/catalog/bigquery.py::_validate_ident checked the same `$`
patterns with `.match` that the quoting chokepoint now checks with
`fullmatch`. So `scope.tables` values and FQN parts such as
"orders\n" reached `table_name IN ('...')`. The ODCS and ODPS
importers compile the bundled schema's identifier and semver
patterns, which end in `$`, and also used `.match`: an imported id
"orders\n" was kept verbatim, and "1.2.3\n" counted as semver. All
three now call `fullmatch`.
The two patterns that keep `$` because their call sites use
`fullmatch` (`_DEFAULT_IDENTIFIER`, `_WORKSPACE_ID_RE`) now say so
in a comment beside them.
These messages quoted the value as '{value}', so a name refused for a
trailing newline printed as a valid-looking name split over two
lines. They now use repr, which prints any other value exactly as
before.
The comment on forge_domain_enrichment._SAFE_DOMAIN_RE says it matches forge_copilot_prompts._PROFILE_NAME_RE, and the comment on _ai_setup_storage._DOMAIN_SLUG_RE says it mirrors _SAFE_DOMAIN_RE. The profile-name guard now ends in `\Z`, so both do too. Neither is reachable with a trailing newline: one runs after a disk lookup, the other on stripped input.
The count in the changelog entry depended on how patterns were grouped. The entry now lists what is covered instead, including the AWS region refusal and the imports. validate_id's summary said it refused a trailing newline "as well", which the shared grammar now does itself.
📄 Documentation ReminderThis PR appears to be missing a documentation reference. Our docs live in a separate repo. Please update the PR description with one of:
See the Contributing Guide for details. |
bigquery_validation imports google.cloud.bigquery at module load. The iac Stage 1 unit lane installs no GCP extra, so the new pin failed collection there with ModuleNotFoundError. It now skips via pytest.importorskip, the guard tests/providers/test_validation_sql_injection.py already applies to the same module.
This was referenced Nov 3, 2026
fas89
added a commit
that referenced
this pull request
Nov 4, 2026
…atch (#816) Python's $ also matches just before a final "\n", so re.match(r"^[a-z]+$", "orders\n") succeeds. #788 and #797 fixed the known validators with \Z or fullmatch, but nothing kept the class from coming back: no ruff, pylint, bandit, dlint or Semgrep CE rule covers it (astral-sh/ruff#15017 proposes one). tests/security/test_no_dollar_anchored_validators.py walks the AST of fluid_build/ and follows the pattern of every .match / .search call back to where it is written: a literal or constant, a dict or tuple, an import, a helper's return value, a property, a re.Pattern parameter, or a pattern compiled at run time. It reads each pattern with re's own parser, so alternatives, groups, escapes and VERBOSE count the way re reads them. Calls whose $ is deliberate are listed in _ACCEPTED with the reason. An entry that no longer matches a call fails as well, so the list only shrinks. On main before #797 it fails on 44 calls, including the shapes the sweep missed: a pattern held in a dict, schema patterns compiled at run time, and a pattern the caller passes in. It also caught the $ + .match sites merged since (#793, #801, #799). Each was read and is listed with its reason.
2 tasks done
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.
Python's
$also matches just before a final newline, sore.match(r"^[a-z]+$", "orders\n")succeeds and the caller goes on to use"orders\n". #788 closed this inproviders/_sql_safety.py. This PR sweeps the rest offluid_build/for the same shape: a whole-string allowlist ending in$, used with.matchor.search, on input that can carry a trailing newline.Inputs like that are easy to produce. A YAML
|block scalar ends in one, so do many.envvalues, andparse_qsdecodes%0A.What changed
31 patterns now end in
\Z. Five more are matched withfullmatchat their call sites:bigquery._validate_ident;The sites cover quality-check selectors, catalog and JDBC identifiers, BigQuery and GCP project ids, contract, build, federation-workspace and overlay-env ids, row-filter names, Singer tap and target names, prompt profile and overlay names, mission predicate paths, AWS and GCP resource names, Snowflake account segments, Databricks git refs, and the ODCS and ODPS import checks. The last two compile the bundled schema's
$patterns.An AWS binding region with whitespace around it is refused (
kind: aws-region-whitespace). Neither of the obvious alternatives is safe:_guard_region_move."us-east-1\n"passesdeniedRegions: [us-east-1].Messages show the refused newline. The AWS name and mission-path refusal messages now use
repr.Docstrings and changelog. The
validate_iddocstring is updated, and CHANGELOG gets an entry under Unreleased / Security.Before, seen through the CLI on clean origin/main:
fluid generate iacwroteprovider.aws = {"region": "eu-west-1\n"}.git_source.branch = "main\n"from?branch=main%0A.Now:
What stays
81 other
$-ending patterns are left as they are, each for a reason:fullmatch.\n.\Zwould loosen the check.$guards no input (\s*$,MULTILINE, captured groups only).quote_identifierquotes and never refuses._recency_windowpasses a duration verbatim, and\Zwould drop a window every downstream parser reads.Prior art (borrow-before-build)
py/regex/unmatchable-dollar, a different defect. The semgrep registry rulecheck-validation-regexis Ruby/Rails only.$changed to\Z.fullmatch.\Zand not\z:\zarrives only in Python 3.14, and this repo supports 3.10 and later.Validation
tests/security/test_trailing_newline_anchors.pyhas 46 cases. Each pins one site with a value and the same value plus"\n". All 46 fail on a clean origin/main checkout, each on its newline assertion, and all 46 pass here.ruff check fluid_build/ tests/, the CI dead-code gate,black==24.10.0 --check, product guardrail, license headers,lint-imports(6 kept), mypy-strict hotspots, bandit, and detect-secrets 1.5.0 on the changed files all pass.tests/perf/test_startup_budget.pyfails 3 tests in this local venv. It fails the same 3, with identical module counts, on clean origin/main. The diff adds no import.Reviews (/ship)
deniedRegionsbypass described above. It is fixed by the refusal, and the final re-review found no findings.bigquery._validate_ident). Fixed._validate_identunpinned) and P3 (AWS_ENDPOINT_URL_*env leak). Fixed.reprand a remediation.bigquery._validate_ident."id\n"while the SodaCL exporter strips it. That divergence pre-dates this PR, and native already failed on main.Follow-ups (not in this PR)
patternkeyword has the same gap. jsonschema runsre.search, sofluid validateacceptsid: "orders\n"and plan/apply now refuse it later. A separate change is in progress._idsis refused only after infrastructure is applied. That pre-dates this PR for leading-digit and over-128-character ids._recency_windowshould strip and return, asodcs/mappers/sla.pydoes.Documentation
fluid generate iac/fluid applywithkind: aws-region-whitespace. The CHANGELOG Security entry records both.