Python点击劫持防护
"""
Python 点击劫持防护 —— 通过 HTTP 响应头防御点击劫持攻击
涵盖 X-Frame-Options、CSP frame-ancestors 和 Django/Flask 中间件实现
"""
# 点击劫持(Clickjacking)攻击者通过透明 iframe 覆盖诱导用户点击
# 防御核心:禁止页面在 iframe 中加载或仅允许同源加载
from typing import Dict, Optional, List
from enum import Enum
# ========== 第一部分:X-Frame-Options 响应头 ==========
class XFrameOptions(Enum):
"""
X-Frame-Options 头的三个可选值:
- DENY:完全禁止在 iframe 中加载
- SAMEORIGIN:仅允许同源页面在 iframe 中加载
- ALLOW-FROM:允许指定来源加载(已废弃,改用 CSP)
"""
DENY = "DENY"
SAMEORIGIN = "SAMEORIGIN"
ALLOW_FROM = "ALLOW-FROM"
# ========== 第二部分:Content-Security-Policy 头 ==========
class CSPDirectiveBuilder:
"""
Content-Security-Policy 的 frame-ancestors 指令构建器。
这是现代浏览器推荐的点击劫持防护方式,比 X-Frame-Options 更灵活。
frame-ancestors 指定哪些来源可以将当前页面嵌入为 iframe。
"""
def __init__(self):
# 默认完全禁止
self._directives: List[str] = []
def deny(self) -> "CSPDirectiveBuilder":
"""
禁止任何页面嵌入(等效于 X-Frame-Options: DENY)。
"""
self._directives = ["'none'"]
return self
def same_origin(self) -> "CSPDirectiveBuilder":
"""
仅允许同源页面嵌入(等效于 X-Frame-Options: SAMEORIGIN)。
"""
self._directives = ["'self'"]
return self
def allow(self, *origins: str) -> "CSPDirectiveBuilder":
"""
允许指定的来源嵌入页面。
来源可以是域名、协议+域名或具体 URL。
"""
for origin in origins:
self._directives.append(origin)
return self
def build(self) -> str:
"""
生成完整的 frame-ancestors 指令字符串。
"""
if not self._directives:
return "frame-ancestors 'none'"
# 'self' 必须在最前面(如果有的话)
directives = sorted(
self._directives,
key=lambda x: (0 if x == "'self'" else 1)
)
return f"frame-ancestors {' '.join(directives)}"
# ========== 第三部分:Flask 中间件实现 ==========
class FrameOptionsMiddleware:
"""
Flask 中间件 —— 自动为响应添加点击劫持防护头。
支持按路由配置不同的防护策略。
"""
def __init__(self, app, default_policy: str = "DENY"):
self.app = app
self.default_policy = default_policy
# 按端点配置例外策略
self._exempt_routes: Dict[str, str] = {}
def add_exemption(self, endpoint: str, policy: str = "SAMEORIGIN"):
"""
为特定端点配置不同的帧策略。
例如支付页面可能需要被允许在特定第三方嵌入。
"""
self._exempt_routes[endpoint] = policy
def __call__(self, environ, start_response):
"""
WSGI 中间件调用入口。
拦截响应并添加安全头。
"""
def _start_response(status, headers, exc_info=None):
# 获取当前请求路径
path = environ.get("PATH_INFO", "/")
# 确定使用的策略
policy = self._exempt_routes.get(path, self.default_policy)
# 添加 X-Frame-Options 头
if policy == "DENY":
headers.append(("X-Frame-Options", "DENY"))
elif policy == "SAMEORIGIN":
headers.append(("X-Frame-Options", "SAMEORIGIN"))
elif policy.startswith("ALLOW-FROM"):
headers.append(("X-Frame-Options", policy))
# 同时添加 CSP frame-ancestors(双保险)
csp_builder = CSPDirectiveBuilder()
if policy == "DENY":
csp_value = csp_builder.deny().build()
elif policy == "SAMEORIGIN":
csp_value = csp_builder.same_origin().build()
elif policy.startswith("ALLOW-FROM"):
# 提取允许的来源
origin = policy.replace("ALLOW-FROM ", "")
csp_value = csp_builder.allow(origin).build()
else:
csp_value = csp_builder.deny().build()
headers.append(("Content-Security-Policy", csp_value))
return start_response(status, headers, exc_info)
return self.app(environ, start_response)
# ========== 第四部分:Django 中间件实现 ==========
class DjangoFrameOptionsMiddleware:
"""
Django 中间件 —— 自动添加帧防护头。
在 Django 的 settings.MIDDLEWARE 中注册使用。
"""
def __init__(self, get_response):
self.get_response = get_response
# 按 URL 前缀配置例外
self._exempt_prefixes: Dict[str, str] = {}
def add_exempt_prefix(self, prefix: str, policy: str = "SAMEORIGIN"):
"""为特定 URL 前缀设置不同的帧策略"""
self._exempt_prefixes[prefix] = policy
def __call__(self, request):
response = self.get_response(request)
# 判断使用哪种策略
policy = self.default_policy
for prefix, exempt_policy in self._exempt_prefixes.items():
if request.path.startswith(prefix):
policy = exempt_policy
break
# 添加安全头
response["X-Frame-Options"] = policy
# 添加 CSP 头(如果尚未设置)
if "Content-Security-Policy" not in response:
csp = self._build_csp(policy)
response["Content-Security-Policy"] = csp
return response
def _build_csp(self, policy: str) -> str:
"""根据策略生成对应的 CSP 值"""
builder = CSPDirectiveBuilder()
if policy == "DENY":
return builder.deny().build()
elif policy == "SAMEORIGIN":
return builder.same_origin().build()
return builder.deny().build()
@property
def default_policy(self) -> str:
return "DENY"
# ========== 第五部分:HTML 级别的额外防护 ==========
def generate_frame_busting_script() -> str:
"""
生成客户端框架破坏(Frame Busting)JavaScript 代码。
作为 HTTP 头的补充防护措施,兼容不支持 X-Frame-Options 的旧浏览器。
注意:某些现代浏览器可能限制这种脚本的执行。
"""
return """
"""
# ========== 第六部分:演示 ==========
def demo_clickjacking_protection():
"""
演示各种点击劫持防护策略的配置和效果。
"""
print("=== 点击劫持防护演示 ===\n")
# 1. 显示 X-Frame-Options 策略
print("--- X-Frame-Options 策略 ---")
for policy in XFrameOptions:
print(f" {policy.value}: {policy.name}")
# 2. 演示 CSP frame-ancestors 构建
print("\n--- CSP frame-ancestors 构建 ---")
builder = CSPDirectiveBuilder()
print(f" DENY: {builder.deny().build()}")
builder = CSPDirectiveBuilder()
print(f" SAMEORIGIN: {builder.same_origin().build()}")
builder = CSPDirectiveBuilder()
print(f" 允许特定来源: {builder.allow('https://trusted.com', 'https://partner.com').build()}")
# 3. 综合建议
print("\n--- 推荐配置 ---")
print("""
生产环境推荐采用"纵深防御"策略:
1. HTTP 头:X-Frame-Options: SAMEORIGIN
2. CSP 头:Content-Security-Policy: frame-ancestors 'self'
3. 敏感页面(如支付):X-Frame-Options: DENY
4. 需要被第三方嵌入的页面:使用 CSP 白名单方式
5. 关键操作增加二次确认步骤(如验证码)
""")
# 4. 生成 Frame Busting 脚本示例
print("--- Frame Busting 脚本(备用防护)---")
print(generate_frame_busting_script()[:100] + "...")
if __name__ == "__main__":
demo_clickjacking_protection()
