Repository navigation
test(security): fail when a whole-string validator ends in $ under .match - #816
Merged
Merged
Conversation
…atch 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.
📄 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. |
The code-quality check flagged re as imported with both import and from-import. re._constants and re._parser are attributes of the re package on 3.11+; 3.10 still falls back to sre_constants / sre_parse.
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.
What and why
Python's
$matches at the end of the string and also just before a final\n, sore.match(r"^[a-z]+$", "orders\n")succeeds and the caller goes on to use"orders\n". #788 and #797 fixed the known whole-string validators with\Zorfullmatch, and #799 fixed the JSON-schemapatternkeyword. Nothing kept the class from coming back, and it already had: #793 and #801, merged after #797, each added a$+.matchvalidator.This adds
tests/security/test_no_dollar_anchored_validators.py, a static gate overfluid_build/. No production file changes.How it works
It reads, it does not import. The scan walks the AST of every module under
fluid_build/, so the gate has no dependency on extras or on import side effects.It judges the call, not the literal. For every
re.match/re.search, every.match/.searchon a compiled pattern, and every method reference (filter(RX.match, names)), it follows the pattern back to where it was written:+orstr.join;.get/.items()/.values(),tuple/dict/MappingProxyType/sorted/enumerate;from . import subin a package__init__;property/cached_property;self.X, an inherited or generic base,type(self),RX.pattern.A call with
fullmatch,sub,findallorsplitis not reported, and neither is a$pattern that only such calls use.The anchor rule reads the pattern with
re's own parser (re._parser, orsre_parseon 3.10). A call is reported when an alternative is anchored at the start (.matchanchors it anyway) and can end at a$. It is not reported underMULTILINE, or when the$follows an unbounded repeat that consumes the newline anyway (\s*$,[^/]*$,(.+)$underDOTALL). So a half-fixed^a$|^b\Z, a$inside a group,VERBOSEand escapes are read the wayrereads them.A pattern the scan cannot read is reported too: one compiled at run time from data (a schema
pattern) and are.Patternparameter the caller fills.Deliberate
$is listed, with the reason._ACCEPTEDis keyed by (file, function, pattern), so a new function that reuses a listed pattern is reported again. 59 entries:\Zwould loosen them;_pattern_keyword).An entry that no longer matches a call fails the test, so the list only shrinks. An entry also pins how many calls it covers (one, unless
_ACCEPTED_CALLSsays otherwise), so a second call with the same pattern in the same function is reported instead of passing on the first one's reason.It cannot pass vacuously. A module it cannot read (syntax, encoding, nesting depth) fails the gate by name, and it must follow more than 150 calls. Name lookups are memoised and each receiver is read once, so long chains stay linear; a test pins this.
Borrow-before-build receipts
py/regex/unmatchable-dollaris a different bug. The Semgrep registry'scheck-validation-regexis Ruby only..semgrep/fluid-rules.ymlis already gated in CI, but Semgrep CE propagates values within one file only, so it cannot follow a pattern through an import or a helper's return, and every deliberate site would need a# nosemgrepin production code.CheckValidationRegexrule (a validator anchors at both ends), the OpenSSF regex guide's advice (\Zorfullmatch), and the shape of this repo's own AST gates (test_no_unencoded_text_io.py,test_no_reserved_tld_defaults.py,fragments/test_no_raw_readers.py).Not covered
An unanchored
.search(r"\.ya?ml$")is a suffix check, not a whole-string validator. The scan does not follow a pattern into a registry filled by.appendor item assignment, into a rule object, into an attribute only a subclass sets, or through+=,getattr,functools.partialor a star import.fluid_build/has no validator of these shapes today. A test pins that the package importsreas a module, sincefrom re import matchwould hide calls from the scan. A pattern built with%orstr.formatcannot be read, so it is reported as built at run time.JSON-schema
patternkeywords run inside jsonschema, out of the scan's sight. FLUID contracts go throughschema_manager.schema_validator(#799); the ODCS, ODPS, OPDS, OSI and extension-schema checks still run stock jsonschema, which #811 addresses.Follow-ups (listed in
_ACCEPTED, not changed here)build_runners/hooks/quality_gate.py::_checkruns a contract's regex gate withre.match, so^[A-Z]{3}$passes"ABC\n". It is not reachable today: nothing reads the hookREGISTRY, and the DuckDB runner checks the gate in SQL withregexp_matches(RE2, where$is the very end). Usefullmatch(or_end_anchored) before the hook is wired.cli/import_workflow/dbt.py::_recency_windowreturnsfluid_windowverbatim; strip it and return the stripped value, assla._as_iso_durationdoes.Validation
635ea30e): 73 passed on Python 3.10, 3.11, 3.12, 3.13 and 3.14.e59e325c): 44 calls, including every validator security: refuse a trailing newline in whole-string validators #797 fixed and the shapes its literal-only sweep missed (bigquery._validate_ident, a dict of patterns; ODCS_root_identifierand ODPS_is_semver, schema patterns compiled at run time;_patterns.validate_and_quote_identifier, a pattern the caller passes);^[a-z]+$+.matchvalidator planted in_ids.py: exactly that one report.iac/providers/aws.py::_hive_decimalandgcp.py::_bq_outcome(fix(iac): column types reach Glue and BigQuery faithfully, or validate refuses them #793;column_types.split_type()removes all whitespace),_databricks_load.staging_volumeand twodatabricks_retentionparsers (feat(databricks): bronze landing, retention, scoped grants and enforcement follow-ups #801; stripped or whitespace-split first),schema_manager._pattern_keyword(security: schema validation let a value with a trailing newline through a pattern #799;_end_anchored()already turns$into\Z).lint-imports(6 kept, 0 broken), product guardrail (0 failing),detect-secrets-hook1.5.0 on the changed file,scripts/mypy_strict.py.tests/security/with-n 4: 417 passed, 5 skipped.--quality-delta: the 20 gating rows are pre-existing dead code; none is on this file. The new scanner's largest functions are at complexity 16–21 (bar 15).Reviews
Security review (Opus judge, own worktree): no blocking findings.
_REMOTE_RE, the federation secret-ref allowlist, Snowflakequote_identifierand_validate_policy_signature, SodaCL,forge_db_tools, the Databricks staging volume and jobs, the DLP detectors, the IP-shape refusals, the overlay rules,schema_manager) and found each reason true. For the sandbox it fuzzed 19,608s3://strings: the groups are identical under$and\Z.fluid_build/**/*.py, writes only undertmp_path, and imports or runs nothing from the package..match/.searchcall (208 with a pattern; the other 7 are memory-store and argparse.search) and every$-ending literal in the package: each is reported, used withfullmatchor.sub, absorbs the newline, or is not a regex.quality_gategap, listed above; the third-party schema checks, which security: the ODCS and plugin-extension checks let a trailing newline through a pattern #811 covers.Verifier (Sonnet, own worktree, before the rewrite): read the cited code for about 40 entries and found every reason true; no difference across Python 3.10–3.14. Its blind spots were fixed (
from . import sub, an exponential call chain, modules dropping out silently, alternation /VERBOSE/ escapes,Patternannotations matched as a substring, containers and wrappers) or listed above under Not covered.