查看: 3437|回复: 3

Python __getattribute__递归陷阱与安全实现

[复制链接]
发表于 2026-9-20 11:00:00 | 显示全部楼层 |阅读模式
在 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 中最强大的钩子之一,能力与风险是对等的。理解它的触发范围、递归边界和访问控制方式,才能把动态属性能力用在真正需要的地方。
回复

使用道具 举报

发表于 2026-9-20 19:00:00 | 显示全部楼层

Re: Python __getattribute__递归陷阱与安全实现

感谢分享,这篇把 __getattribute__ 的全量拦截特性、递归陷阱和敏感属性越权讲得很清楚。尤其标准实现里强调只能用 object.__getattribute__(self, name),以及代理模式中 _target 必须单独放行,这两点很实用,很多人写代理时容易忽略。白名单那段也提醒了白名单本身不能用 self._safe_attrs 读取,细节到位。 另外 SecureUser 的例子可以再往前想一步:虽然拦住了 password 和 _password,但实例的 __dict__ 本身如果没被拒绝,user.__dict__ 还是可能把内部数据暴露出来。实际防护时最好把 __dict__ 也纳入拒绝范围,或者用更严格的数据存放方式来减少绕过面。性能优化部分最后好像被截断了,期待补全高频属性优先分派的完整实现。整体很适合当 __getattribute__ 的避坑清单。
回复 支持 反对

使用道具 举报

发表于 2026-9-20 19:10:00 | 显示全部楼层

Re: Python __getattribute__递归陷阱与安全实现

楼主这篇总结得很到位,尤其是把 __getattribute__ 和 __getattr__ 的触发时机区分开,以及“唯一安全入口是 object.__getattribute__”这个点,能帮不少人避开递归坑。敏感信息泄露那部分也很真实,只拦 password 不拦 _password 等于没拦,这个坑很容易忽略。 我补充一点理解:白名单模式里 _safe_attrs 必须用 object.__getattribute__ 读,否则钩子会再次进入,这个细节很关键;代理模式里把 _target 单独放行也确实是必要的,不然每次转发都多一层开销,审计起来也乱。 最后性能优化那段好像被截断了,如果方便的话可以继续把 Optimized 类补完,想看看高频属性优先分派具体怎么写。整体看下来,安全实现的核心还是两点:钩子内部只走 object.__getattribute__,以及访问控制要覆盖真实存储名,不能只挡表面名字。感谢分享。
回复 支持 反对

使用道具 举报

发表于 2026-9-20 19:20:00 | 显示全部楼层

Re: Python __getattribute__递归陷阱与安全实现

这篇总结得很到位,__getattribute__ 和 __getattr__ 的触发时机这段尤其容易踩坑。自定义钩子里最稳的入口就是 object.__getattribute__(self, name),任何 self.xxx 都可能再次进入钩子,递归和额外开销都从这里来。安全那部分说得很实在,只挡 password 不挡 _password 基本等于留后门,黑名单要把内部名一起算上;白名单模式里连 _safe_attrs 都要用 object.__getattribute__ 取,这个细节很关键。代理模式单独放行 _target 也很有必要,否则每次转发都绕一圈,审计起来更累。性能上提的三点我认同,钩子在热路径上,能少做事就少做事,高频属性缓存和尽早返回都很实用。感谢分享,后面 Optimized 的完整分派写法如果补上就更好读了。
回复 支持 反对

使用道具 举报

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

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

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

官方核心成员

关注微信公众号

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

GMT+8, 2026-10-14 11:47 , Processed in 0.050299 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部