Harden expression sanitization with AST validation - #564
Harden expression sanitization with AST validation#564nevercodecorrect wants to merge 3 commits into
Conversation
Replace regex-based expression filtering with an AST allowlist and evaluate sanitized expressions without Python builtins. Resolve sanitize=None consistently across all compilation and evaluation paths, including disable_cache and direct NumExpr usage. Preserve supported syntax and compatibility for conditional expressions, identity and membership comparisons, and variable names containing double underscores. Retain native SyntaxError behavior for parser failures and TypeError for unknown functions. Add regression tests and release notes for the sanitizer changes.
There was a problem hiding this comment.
Pull request overview
This PR hardens NumExpr’s expression sanitization by replacing the prior regex-based filtering with an AST-based allowlist, and by evaluating sanitized expressions with Python builtins disabled. It also standardizes how sanitize=None resolves across cached, disable_cache=True, and direct NumExpr/compiler entry points, with regression tests and release-note updates.
Changes:
- Replace regex sanitizer with AST validation and block non-allowlisted syntax, dunder names, non-
.real/.imagattribute access, and calls to unknown functions. - Disable Python builtins during
eval()when sanitization is enabled, and ensuresanitize=Noneconsistently resolves to the secure default (unlessNUMEXPR_SANITIZE=0). - Add regression tests covering bypass attempts, caching interactions, and compatibility behaviors (conditional expressions, identity/membership comparisons, double-underscore variable names).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| RELEASE_NOTES.rst | Documents the sanitizer hardening and resulting error/behavior changes. |
| numexpr/tests/test_numexpr.py | Adds regression tests for sanitizer bypasses, cache/no-cache consistency, and accepted syntax forms. |
| numexpr/necompiler.py | Implements AST allowlist sanitizer, builtins-disabled evaluation, and consistent sanitize=None resolution across compilation/evaluation paths. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| sanitize: bool | ||
| `validate` (and by extension `evaluate`) call `eval(ex)`, which is | ||
| potentially dangerous on non-sanitized inputs. As such, NumExpr by default | ||
| performs simple sanitization, banning the characters ':;[', the | ||
| dunder '__[\w+]__', and attribute access to all but '.real' and '.imag'. | ||
| permits only AST nodes used by its expression language, functions from | ||
| its function table, and the attributes '.real' and '.imag'. |
|
Besides copilot review, I'd like to make sure that the new AST parser is not slowing down things wrt the existent regular expression checks. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
In terms of performance, the AST parser would be slower than the regex-based option. But it provides better sanitization and avoids payload bypassing.
The small uncached overhead appears to be the tradeoff for replacing the regex blacklist with structural AST validation. The new sanitizer uses an explicit allowlist of expression-language AST nodes, restricts calls to NumExpr’s function table, and permits only .real and .imag attribute access. The AST-based implementation provides a stronger security boundary than enumerating dangerous textual patterns with regular expressions. Execution command is AST-based logs Number of iterations 100. Length of the array: 10
* Numexpr with non-cacheable expressions: .......... done in 0.74 seconds
* Numexpr with cacheable expressions: .......... done in 0.008 seconds
* Numpy with non-cacheable expressions: .......... done in 0.015 seconds
* Numpy with cacheable expressions: .......... done in 0.014 seconds
Number of iterations 100. Length of the array: 10
* Numexpr with non-cacheable expressions: .......... done in 0.755 seconds
* Numexpr with cacheable expressions: .......... done in 0.008 seconds
* Numpy with non-cacheable expressions: .......... done in 0.018 seconds
* Numpy with cacheable expressions: .......... done in 0.015 seconds
Number of iterations 100. Length of the array: 10
* Numexpr with non-cacheable expressions: .......... done in 0.751 seconds
* Numexpr with cacheable expressions: .......... done in 0.008 seconds
* Numpy with non-cacheable expressions: .......... done in 0.014 seconds
* Numpy with cacheable expressions: .......... done in 0.014 seconds
Number of iterations 100. Length of the array: 10
* Numexpr with non-cacheable expressions: .......... done in 0.72 seconds
* Numexpr with cacheable expressions: .......... done in 0.008 seconds
* Numpy with non-cacheable expressions: .......... done in 0.015 seconds
* Numpy with cacheable expressions: .......... done in 0.015 seconds
Number of iterations 100. Length of the array: 10
* Numexpr with non-cacheable expressions: .......... done in 0.724 seconds
* Numexpr with cacheable expressions: .......... done in 0.008 seconds
* Numpy with non-cacheable expressions: .......... done in 0.015 seconds
* Numpy with cacheable expressions: .......... done in 0.014 secondsregex-based logs: |
Replace regex-based expression filtering with an AST allowlist and evaluate sanitized expressions without Python builtins.
The previous regex-based solution can be bypassed to execute additional code.
Resolve sanitize=None consistently across all compilation and evaluation paths, including disable_cache and direct NumExpr usage.
Preserve supported syntax and compatibility for conditional expressions, identity and membership comparisons, and variable names containing double underscores. Retain native SyntaxError behavior for parser failures and TypeError for unknown functions.
Add regression tests and release notes for the sanitizer changes.