fix: strengthen deserialization allowlist checks to prevent bypass vulnerabilities - #12028
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||
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.
Related Issues
Proposed Changes:
Fixes a deserialization-allowlist bypass that could lead to arbitrary code execution when loading an untrusted pipeline (
Pipeline.load/Pipeline.loads/Pipeline.from_dict).Root cause. The module allowlist was enforced against a string prefix of the attacker-controlled serialized handle, while the callable/class resolvers then walked attributes from the deepest importable module. Because the pattern
haystackmatches anyhaystack.*string, a fabricated handle likehaystack.utils.auth.os.systempassed the allowlist (it has thehaystackprefix), and the resolver imported the realhaystack.utils.authmodule and walked.os.systeminto the standard-libraryosmodule — sinceauth.pydoesimport osat module scope.The dangerous-builtin denylist only covers members of
builtins, soos.system/os.popen/ast.literal_evalreached this way were not blocked.Fix. The allowlist is now enforced against the module each object is actually resolved from, not against a string prefix of the handle:
haystack/core/serialization_security.py: new shared helper_check_resolved_module_allowed()that gates on the resolved object's real module (__name__for modules,__module__for classes/functions). Genuine builtins are exempted (they are gated authoritatively elsewhere and can report a private__module__, e.g.open.__module__ == "_io"). A symbol whose__module__is the private C accelerator of the same allowlisted module it was resolved from (e.g.operator.add->_operator,io.StringIO->_io) is still accepted, so allowlisting a public module keeps working.haystack/utils/callable_serialization.py:deserialize_callablenow (1) only imports module candidates that are on the allowlist — checked before import, so no import-time side effects of an untrusted module — (2) re-checks every module hop during the attribute walk (blocks the... .os.systemescape), and (3) gates the final resolved callable's real module.haystack/utils/type_serialization.py:_import_class_by_namere-checks the resolved class/module's real module, closing the re-export bypass and a module-leak wherehaystack.utils.auth.osreturned theosmodule as if it were a class.How did you test it?
test/core/test_serialization_security.py:TestModuleAttributeWalkBypass— the real gadgets (haystack.utils.auth.os.system,os.popen,output_adapter.ast.literal_eval), the class/type module-leak, a plain-attribute defense-in-depth case, a legit-still-resolves case, and an end-to-endPipeline.loadsrejection of an attackerOutputAdaptercustom_filterspayload.TestPrivateBackingModuleCompatibility—operator.add,functools.reduce, andio.StringIOresolve when the public module is allowlisted, and the private-backing exemption is scoped to the matching module (a symbol resolving to a different private module stays blocked).OutputAdapter, tools, hooks, pipeline): 761 passed.Pipeline.loads; confirmed private-backing modules resolve.hatch run fmtandhatch run test:typesboth pass.Notes for the reviewer
deserialize_callableplus the resolved-__module__gate; the other steps are defense-in-depth.Checklist
fix:,feat:,build:,chore:,ci:,docs:,style:,refactor:,perf:,test:and added!in case the PR includes breaking changes.