Summary
pydash 6.0.0 (2023-01-29) patched CVE-2023-26145 by blocking all dunder key
traversal. However, pydash 6.0.2 (2023-02-24) relaxed this to a narrow
denylist — RESTRICTED_KEYS = ("__globals__", "__builtins__") — re-enabling
__class__ traversal. An attacker who controls the path argument to
pydash.set_() on a class instance target can modify class-level attributes,
affecting all current and future instances of the target class.
Affected Versions
- pydash >=6.0.2, <=8.0.6 —
RESTRICTED_KEYS denylist only blocks
__globals__ and __builtins__; __class__ traversal allowed
- pydash 6.0.0, 6.0.1 — all dunder keys blocked (not affected by this bypass)
- pydash <6.0.0 — no dunder restriction at all (original CVE-2023-26145)
Version Verification
| Version |
__class__ |
__globals__ |
Mechanism |
| <6.0.0 |
ALLOWED |
ALLOWED |
No filter |
| 6.0.0 |
BLOCKED |
BLOCKED |
All dunder keys blocked |
| 6.0.1 |
BLOCKED |
BLOCKED |
Same |
| 6.0.2~8.0.6 |
ALLOWED |
BLOCKED |
RESTRICTED_KEYS denylist (regression) |
Verified by installing each version and testing pydash.set_(obj, '__class__.attr', val).
CVSS (Self-Assessed)
CVSS 3.1: 7.5 (HIGH) — self-assessed, not assigned by upstream or NVD.
AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:L
- Attack Vector: Network (user-controlled path in web applications)
- Attack Complexity: High (requires application to pass user input as
path
to set_() on an object — not dict — target)
- Scope: Changed (class pollution affects all instances, not just the target)
CWE
- CWE-915: Improperly Controlled Modification of Dynamically-Determined
Object Attributes
Root Cause
In pydash 6.0.2, the all-dunder blocking logic from 6.0.0 was replaced with
a narrow denylist in src/pydash/helpers.py:
RESTRICTED_KEYS = ("__globals__", "__builtins__")
The check in _raise_if_restricted_key() only blocks these two keys. All
other dunder attributes — notably __class__ — are freely traversable via
pydash.set_().
|
CVE-2023-26145 (pre-6.0.0) |
This Finding (>=6.0.2) |
| Path |
__init__.__globals__.<target> |
__class__.<class_attr> |
| Blocked? |
Yes (since 6.0.0) |
No |
| Impact |
Module namespace access, RCE |
Class attribute pollution |
| Scope |
Single module's globals |
All instances of target class |
Impact
The following impacts are conditional — they require an application that
passes user-controlled input as the path argument to pydash.set_() with a
class instance (not dict) target. Exploitability depends on the application's
object model and how the polluted attributes are consumed.
1. Privilege Escalation (conditional on class-level auth attributes)
class User:
is_admin = False
victim = User()
pydash.set_(victim, "__class__.is_admin", True)
# ALL instances are now admin — past, present, and future
new_user = User()
assert new_user.is_admin == True # POLLUTED
2. Config Poisoning (conditional on class-level config attributes)
class AppConfig:
secret_key = "production-key-xxx"
debug = False
config = AppConfig()
pydash.set_(config, "__class__.secret_key", "attacker-key")
pydash.set_(config, "__class__.debug", True)
# All new AppConfig() instances inherit poisoned values
3. Denial of Service
class Service:
status = "running"
svc = Service()
pydash.set_(svc, "__class__.__setattr__", "not_callable")
# ALL future setattr calls on any Service instance crash with TypeError
Service().status = "x" # TypeError: 'str' object is not callable
4. Module/Class Identity Spoofing (conditional on trust-check mechanism)
Overwriting __module__ or __qualname__ may bypass trust checks that
inspect class identity, depending on the application's enforcement logic:
pydash.set_(obj, "__class__.__module__", "trusted.module")
pydash.set_(obj, "__class__.__qualname__", "TrustedClass")
Denylist Coverage Analysis
| Path |
Status |
Impact |
__globals__ |
BLOCKED |
(CVE-2023-26145 fix, retained in 6.0.2+) |
__builtins__ |
BLOCKED |
(same) |
__class__.x |
ALLOWED |
Class attribute override |
__class__.__setattr__ |
ALLOWED |
DoS via setattr crash |
__class__.__module__ |
ALLOWED |
Module identity spoof (conditional) |
__class__.__qualname__ |
ALLOWED |
Class name spoof (conditional) |
__class__.__doc__ |
ALLOWED |
Docstring override |
Prerequisites
The vulnerability requires both conditions:
- Application uses
pydash.set_(obj, path, value) where obj is a class
instance (not a plain dict — dict targets are not affected)
- The
path argument is user-controlled (from HTTP request, config file,
CLI argument, etc.)
Known Usage Patterns
The following are patterns observed in public repositories where
pydash.set_() is called with potentially user-influenced paths. These
have not been independently verified as exploitable end-to-end:
- CTF challenges: Multiple CTF competitions (idekCTF 2022, Hology 2023,
Imaginary CTF 2023) have used pydash.set_(obj, user_path, val) as the
intended exploitation primitive. These targeted pre-6.0.0 versions using the
__globals__ path; the __class__ bypass in >=6.0.2 has not been
documented in any public CTF writeup.
- LunarLink (tahitimoon/LunarLink):
pydash.set_(request_data, path, v)
in HTTP test runner. Target type unconfirmed (may be dict).
Suggested Fix
Replace the narrow denylist with a pattern-based check, restoring the
6.0.0 behavior of blocking all dunder keys:
# In src/pydash/helpers.py
def _raise_if_restricted_key(key):
if key.startswith("__") and key.endswith("__"):
raise KeyError(
f"access to dunder key {key!r} is not allowed"
)
This matches the approach used by django-unicorn's CVE-2024-22340 patch.
Alternative: Allowlist Approach
For maximum security, restrict path traversal to only allow keys that don't
start with underscore:
def _raise_if_restricted_key(key):
if key.startswith("_"):
raise KeyError(
f"access to private/protected key {key!r} is not allowed"
)
Timeline
- 2023-01-29: pydash 6.0.0 released — blocks all dunder key traversal
(CVE-2023-26145 fix)
- 2023-02-24: pydash 6.0.2 released — relaxes to
RESTRICTED_KEYS denylist,
re-enabling __class__ traversal (regression)
- 2023-09-28: CVE-2023-26145 published
- 2026-03-16:
__class__ bypass in >=6.0.2 discovered and verified
- 2026-03-16: PoC developed and tested on pydash 8.0.6
References
Credit
Discovered via differential fuzzing and static analysis using web-fuzzer
class pollution scanner.
Summary
pydash 6.0.0 (2023-01-29) patched CVE-2023-26145 by blocking all dunder key
traversal. However, pydash 6.0.2 (2023-02-24) relaxed this to a narrow
denylist —
RESTRICTED_KEYS = ("__globals__", "__builtins__")— re-enabling__class__traversal. An attacker who controls thepathargument topydash.set_()on a class instance target can modify class-level attributes,affecting all current and future instances of the target class.
Affected Versions
RESTRICTED_KEYSdenylist only blocks__globals__and__builtins__;__class__traversal allowedVersion Verification
__class____globals__RESTRICTED_KEYSdenylist (regression)Verified by installing each version and testing
pydash.set_(obj, '__class__.attr', val).CVSS (Self-Assessed)
CVSS 3.1: 7.5 (HIGH) — self-assessed, not assigned by upstream or NVD.
AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:Lpathto
set_()on an object — not dict — target)CWE
Object Attributes
Root Cause
In pydash 6.0.2, the all-dunder blocking logic from 6.0.0 was replaced with
a narrow denylist in
src/pydash/helpers.py:The check in
_raise_if_restricted_key()only blocks these two keys. Allother dunder attributes — notably
__class__— are freely traversable viapydash.set_().Difference from CVE-2023-26145
__init__.__globals__.<target>__class__.<class_attr>Impact
The following impacts are conditional — they require an application that
passes user-controlled input as the
pathargument topydash.set_()with aclass instance (not dict) target. Exploitability depends on the application's
object model and how the polluted attributes are consumed.
1. Privilege Escalation (conditional on class-level auth attributes)
2. Config Poisoning (conditional on class-level config attributes)
3. Denial of Service
4. Module/Class Identity Spoofing (conditional on trust-check mechanism)
Overwriting
__module__or__qualname__may bypass trust checks thatinspect class identity, depending on the application's enforcement logic:
Denylist Coverage Analysis
__globals____builtins____class__.x__class__.__setattr____class__.__module____class__.__qualname____class__.__doc__Prerequisites
The vulnerability requires both conditions:
pydash.set_(obj, path, value)whereobjis a classinstance (not a plain dict — dict targets are not affected)
pathargument is user-controlled (from HTTP request, config file,CLI argument, etc.)
Known Usage Patterns
The following are patterns observed in public repositories where
pydash.set_()is called with potentially user-influenced paths. Thesehave not been independently verified as exploitable end-to-end:
Imaginary CTF 2023) have used
pydash.set_(obj, user_path, val)as theintended exploitation primitive. These targeted pre-6.0.0 versions using the
__globals__path; the__class__bypass in >=6.0.2 has not beendocumented in any public CTF writeup.
pydash.set_(request_data, path, v)in HTTP test runner. Target type unconfirmed (may be dict).
Suggested Fix
Replace the narrow denylist with a pattern-based check, restoring the
6.0.0 behavior of blocking all dunder keys:
This matches the approach used by django-unicorn's CVE-2024-22340 patch.
Alternative: Allowlist Approach
For maximum security, restrict path traversal to only allow keys that don't
start with underscore:
Timeline
(CVE-2023-26145 fix)
RESTRICTED_KEYSdenylist,re-enabling
__class__traversal (regression)__class__bypass in >=6.0.2 discovered and verifiedReferences
Credit
Discovered via differential fuzzing and static analysis using web-fuzzer
class pollution scanner.