Skip to content

Security: pydash RESTRICTED_KEYS Regression via __class__ Traversal #240

@dmbs335

Description

@dmbs335

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.6RESTRICTED_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_().

Difference from CVE-2023-26145

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:

  1. Application uses pydash.set_(obj, path, value) where obj is a class
    instance
    (not a plain dict — dict targets are not affected)
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions