查看: 286|回复: 0

Python __getattribute__递归陷阱与安全实现

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在 Python 面向对象编程中,属性的读取并不是一次简单的字典查找。只要类里定义了 __getattribute__,任何通过实例进行的点号访问(obj.attr)都会先进入这个方法,已经存在的属性也不例外。它和 __getattr__ 的触发时机并不相同:__getattr__ 只在常规查找失败之后才被调用,而 __getattribute__ 会拦截全部属性访问请求。正因为这种“全量拦截”的能力,它常被用于属性代理、动态计算和访问审计;也正因为如此,递归失控和敏感属性越权读取这两类问题最容易在这里出现。

1. 标准实现形式

自定义 __getattribute__ 的骨架非常固定:执行自己的处理逻辑,然后交回基类完成真正的查找。
  1. def __getattribute__(self, name):
  2.     # 自定义处理逻辑
  3.     return object.__getattribute__(self, name)
复制代码

这里 object.__getattribute__(self, name) 是唯一安全的取值入口。原因很直接:在该方法内部再用 self.xxx 去取任何东西,都会重新进入 __getattribute__ 本身。

2. 两类常见安全隐患

2.1 无限递归陷阱

最常见的写法错误,就是在本方法里用属性访问的方式取属性:
  1. class BadExample:
  2.     def __getattribute__(self, name):
  3.         return self.name  # 这将导致无限递归
复制代码

self.name 会再次触发 __getattribute__,于是形成 self.name -> __getattribute__ -> self.name 的循环,最终以 RecursionError 收场。正确的做法是把取值动作交给基类:
  1. class SafeExample:
  2.     def __getattribute__(self, name):
  3.         # 使用object.__getattribute__避免递归
  4.         value = object.__getattribute__(self, name)
  5.         # 自定义处理逻辑
  6.         return value
复制代码

2.2 敏感信息泄露

第二类问题更隐蔽。下面这个类本意是禁止读取 password:
  1. class User:
  2.     def __init__(self, username, password):
  3.         self._username = username
  4.         self._password = password  # 敏感信息
  5.     def __getattribute__(self, name):
  6.         if name == 'password':
  7.             raise AttributeError("Access denied")
  8.         return object.__getattribute__(self, name)
复制代码

拦截只覆盖了 password 这个名字,而真实存放数据的 _password 依旧可以通过 user._password 直接读到,保护形同虚设。更稳妥的写法是把实例数据直接写进 __dict__,并把带下划线的内部名一并纳入拒绝名单:
  1. class SecureUser:
  2.     def __init__(self, username, password):
  3.         self.__dict__['_username'] = username
  4.         self.__dict__['_password'] = password
  5.     def __getattribute__(self, name):
  6.         if name in ('_password', 'password'):
  7.             raise AttributeError("Access denied")
  8.         return object.__getattribute__(self, name)
复制代码

可以看出,拦截名单必须同时包含对外名和内部名,否则绕过只需要换一个前缀。

3. 安全实现模式

3.1 白名单模式

如果只允许外部访问少数几个属性,白名单比黑名单更容易维护:
  1. class WhitelistExample:
  2.     _safe_attrs = ['name', 'age']
  3.     def __getattribute__(self, name):
  4.         if name not in object.__getattribute__(self, '_safe_attrs'):
  5.             raise AttributeError(f"'WhitelistExample' object has no attribute '{name}'")
  6.         return object.__getattribute__(self, name)
复制代码

注意白名单本身也必须用 object.__getattribute__(self, '_safe_attrs') 读取,写成 self._safe_attrs 会重新进入钩子。

3.2 属性代理模式

代理对象需要把大部分属性转发给被代理的目标,同时对内部字段做特殊处理:
  1. class Proxy:
  2.     def __init__(self, target):
  3.         self._target = target
  4.     def __getattribute__(self, name):
  5.         if name == '_target':
  6.             return object.__getattribute__(self, name)
  7.         return getattr(object.__getattribute__(self, '_target'), name)
复制代码

_target 必须单独放行:若写成 self._target,每次转发都会重新走一遍钩子,虽然在该分支下不会无限递归,但会徒增开销,也让访问路径更难审计。其余属性统一通过 getattr 转发到目标对象。

4. 性能优化要点

钩子拦截的是每一次属性访问,实现不当会直接把热路径拖慢。可用的优化方向有三个:不要在 __getattribute__ 中做复杂计算;对频繁访问的属性用 @property 做缓存;对不需要特殊处理的属性尽早返回。下面是一个按高频属性优先分派的写法:
  1. class Optimized:
  2.     def __getattribute__(self, name):
  3.         if name in ('x', 'y', 'z'):  # 高频访问属性
  4.             return object.__getattribute__(self, f'_{name}')
  5.         # 其他属性处理逻辑
复制代码

该分支把 x、y、z 映射到实例上对应的 _x、_y、_z,其余属性的处理逻辑需要接在这个分支之后补充。判断次数越少、越早返回,属性访问的额外成本就越低。

5. 测试与调试建议

测试 __getattribute__ 时,至少覆盖以下几点:所有属性访问路径都要走到;递归保护是否真的生效;异常抛出是否符合预期;整体性能是否可以接受。

调试可以用三招:在 __getattribute__ 中添加日志打印,看清每次访问的属性名;用 pdb.set_trace() 在钩子入口中断;直接查看实例的 __dict__,确认属性真正存放在哪里。

6. 两个实际应用案例

6.1 属性访问日志
  1. class LoggedAccess:
  2.     def __getattribute__(self, name):
  3.         print(f"Accessing attribute: {name}")
  4.         try:
  5.             return object.__getattribute__(self, name)
  6.         except AttributeError as e:
  7.             print(f"Failed to access {name}: {e}")
  8.             raise
复制代码

成功的访问和失败的访问都会被记录下来,异常在打印之后继续向上抛出,不改变原有的报错行为。

6.2 动态属性计算
  1. class Circle:
  2.     def __init__(self, radius):
  3.         self.radius = radius
  4.     def __getattribute__(self, name):
  5.         if name == 'area':
  6.             import math
  7.             r = object.__getattribute__(self, 'radius')
  8.             return math.pi * r ** 2
  9.         return object.__getattribute__(self, name)
复制代码

area 并不存在于实例字典中,而是每次访问时按当前半径重新计算;radius 同样要用 object.__getattribute__ 取出,避免递归。如果这个属性会被高频读取,更合适的做法是改用 @property 并配合缓存。

7. 最佳实践小结

- 取属性一律通过 object.__getattribute__,不要在本方法内使用 self.xxx。
- 对敏感属性实施严格的访问控制,对外名和内部下划线别名都要拦截。
- 关注性能影响,避免在钩子中做不必要的处理。
- 为 __init__、__new__ 等特殊方法保留访问路径。
- 把 __getattribute__ 的行为写进文档。
- 在单元测试中覆盖所有边界情况。
- 只在确实需要拦截全部属性访问时才实现它;否则 @property 或 __getattr__ 往往是更安全的选择。

__getattribute__ 是 Python 中最强大的钩子之一,能力与风险是对等的。理解它的触发范围、递归边界和访问控制方式,才能把动态属性能力用在真正需要的地方。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-20 13:08 , Processed in 0.020490 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部