Skip to content

Admin Bot 与 XSS 跨站脚本纵深防御 ​

针对 Admin Bot 的 XSS 防御不能只停留在“拦截 <script> 标签”。即便拦截了内联脚本,攻击者仍可通过 <svg><animate>、<math><mtext> 触发 Mutation XSS(mXSS)绕过净化器,或者利用 DOM Clobbering 劫持全局配置,再配合 img-src / form-action / DNS / 同源内网时间差外带 HttpOnly 背后的同源接口数据(如 /flag、/api/me)。防御必须兼顾渲染上下文隔离、严格 CSP + Trusted Types、DOM 命名空间保护与 Bot 运行时沙盒隔离。

防御工作流 ​

text
输出阶段按 HTML/属性/JS/URL 上下文编码或同构 DOMPurify 净化
  → 部署 Strict Nonce CSP + Trusted Types 阻断内联脚本与 DOM Sink
  → 消除 DOM Clobbering(ID 前缀隔离 + 显式类型与属性校验)
  → 凭据全部落 HttpOnly + SameSite=Strict Cookie,禁用 Storage 存敏
  → 收紧 Admin Bot URL 准入白名单、出口防火墙与 127.0.0.1 内网隔离

1. 上下文编码与 mXSS 净化加固 ​

  1. 禁止手写正则清洗 HTML:黑名单过滤 onerror、script 会被 <math><mtext><table><mglyph><style> 等嵌套命名空间切换(HTML5 parsing vs foreign content)轻松绕过。
  2. 富文本同构安全配置:使用最新维护的 DOMPurify(并保持服务端与客户端一致的解析版本),在不需要矢量动画或公式的场景下显式禁用高危命名空间与表单标签:
    javascript
    const clean = DOMPurify.sanitize(dirtyHtml, {
      USE_PROFILES: { html: true },          // 禁用 svg 与 mathml 命名空间,阻断 <svg><animate> / <math> mXSS
      FORBID_TAGS: ["style", "form", "input", "iframe", "base", "object"],
      FORBID_ATTR: ["style", "formaction"],
      SANITIZE_DOM: true,                    // 防御 DOM Clobbering 对 name/id 的劫持
      ALLOW_UNKNOWN_PROTOCOLS: false
    });
  3. URL 属性协议白名单:对 <a href>、<img src>、重定向地址强制校验解析后协议仅允许 https:、http:(绝对拒绝 javascript:、data:、vbscript:)。

2. Strict CSP 与 Trusted Types 阻断外带与 Gadget ​

CSP 不应仅限制脚本执行,还必须同步封堵 connect-src、img-src、form-action 与 base-uri 四条外带与劫持通道:

http
Content-Security-Policy:
  default-src 'none';
  script-src 'nonce-<CRYPTO_RANDOM_128BIT>' 'strict-dynamic';
  style-src 'self' 'nonce-<CRYPTO_RANDOM_128BIT>';
  img-src 'self';
  connect-src 'self';
  form-action 'self';
  base-uri 'none';
  object-src 'none';
  frame-ancestors 'none';
  require-trusted-types-for 'script';

关键配置要点:

  • 禁止 'unsafe-inline' 与宽泛 'self' JSONP/上传 JS:使用每次响应动态生成的密码学随机 nonce 配合 'strict-dynamic',防止同源上传文件或遗留 JSONP 端点变成 script-src 'self' 绕过 Gadget。
  • 锁定 base-uri 'none' 与 form-action 'self':防止注入 <base href="https://evil.example"> 劫持相对路径脚本加载,或通过隐藏表单 POST 外带长文本。
  • 收紧 img-src 'self' 与 connect-src 'self':切断 new Image().src、navigator.sendBeacon、跨域 fetch 和 WebSocket 外带。
  • 启用 Trusted Types:强制 innerHTML、insertAdjacentHTML、eval、script.src 等高危 DOM Sink 只接受经统一受控 Policy(内嵌 DOMPurify)生成的 TrustedHTML / TrustedScriptURL。

3. DOM Clobbering 专项防御 ​

当注入只能插入无脚本 HTML(如 <form id=config><input name=isAdmin value=true>)时,传统 window.config || defaultConfig 会被 HTMLFormElement 覆盖:

  • ID / Name 前缀隔离:所有用户生成的 HTML 元素 id 与 name 属性在净化阶段强制加前缀(如 user-content-),使其无法碰撞代码中的全局标识(config、redirect_uri、csrf_token)。
  • 显式对象与类型检查:读取配置前先冻结真实配置对象 Object.freeze(window.__APP_CONFIG__),或校验目标变量不是 DOM 节点(!(val instanceof Element) 且 typeof val.nodeType !== "number")。

4. Admin Bot 浏览器与网络沙盒隔离 ​

Admin Bot 往往具备公网不可达的内网视角与高权限会话,必须实施运行时隔离:

  1. 会话凭据属性硬化:Bot 使用的管理员会话 Cookie 强制设置 HttpOnly; Secure; SameSite=Strict,严禁把 JWT、API Token 写入 localStorage 或 sessionStorage,降低单次 XSS 后凭据被直接窃取的半径。
  2. 提交 URL 准入校验:/report、/submit、/visit 接口在服务端严格解析提交 URL:仅允许 https:(或限定 http:)且 hostname 必须精确匹配受保护业务域名白名单,拒绝 file:、javascript:、data: 与内网 IP(127.0.0.1、169.254.169.254、10.0.0.0/8、::1)。
  3. Headless 容器出站网络管控:Bot 运行容器配置独立 Egress 网络策略(NetworkPolicy / iptables),阻断对外部未知监听器域名/IP 的连接与 DNS 解析,同时隔离同主机回环敏感管理端口(如 6379、9222 远程调试端口仅绑定受控 Unix Socket 或关闭远程调试),防止利用浏览器做内网端口时间差探测。

5. 可运行 CSP 策略与 Bot URL 准入校验脚本 ​

以下 Python 代码可用于自动化核查响应头 CSP 配置完整性(检测缺失 base-uri、form-action、宽松 img-src/connect-src、unsafe-inline 等缺陷),并验证 Admin Bot 提交接口的 URL/内网地址过滤逻辑:

python
#!/usr/bin/env python3
"""CSP 外带通道核查器与 Admin Bot 提交 URL 白名单校验器。"""
import ipaddress
from urllib.parse import urlparse

ALLOWED_BOT_HOSTS = {"app.example.com", "portal.example.com"}


def audit_csp_header(csp: str) -> list[str]:
    """核查 CSP 是否阻断脚本注入与常见外带通道。"""
    directives: dict[str, list[str]] = {}
    for part in csp.split(";"):
        tokens = part.strip().split()
        if tokens:
            directives[tokens[0].lower()] = [t.lower() for t in tokens[1:]]

    findings: list[str] = []
    script_src = directives.get("script-src") or directives.get("default-src") or []
    if not script_src or "'unsafe-inline'" in script_src or "'unsafe-eval'" in script_src or "*" in script_src:
        findings.append("weak script-src: allows unsafe-inline, unsafe-eval, wildcard, or missing")
    if not any(t.startswith("'nonce-") for t in script_src):
        findings.append("script-src should enforce cryptographic nonce")

    for chan in ("connect-src", "img-src", "form-action"):
        vals = directives.get(chan) or directives.get("default-src") or []
        if not vals or "*" in vals or "https:" in vals or "http:" in vals:
            findings.append(f"exfiltration channel {chan} is overly permissive: {vals}")

    if directives.get("base-uri") not in (["'none'"], ["'self'"]):
        findings.append("missing strict base-uri ('none' or 'self')")
    if "require-trusted-types-for" not in directives:
        findings.append("Trusted Types not enforced (missing require-trusted-types-for 'script')")
    return findings


def validate_bot_target_url(raw_url: str) -> tuple[bool, str]:
    """校验提交给 Admin Bot 访问的 URL,阻断非 HTTP(S)、私有 IP 与非白名单域名。"""
    parsed = urlparse(raw_url.strip())
    if parsed.scheme.lower() not in {"http", "https"}:
        return False, f"blocked scheme: {parsed.scheme}"
    host = (parsed.hostname or "").lower()
    if not host:
        return False, "missing hostname"

    # 阻断 IP 字面量与回环/云元数据地址
    try:
        ip = ipaddress.ip_address(host)
        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
            return False, f"blocked internal/reserved IP: {host}"
    except ValueError:
        pass

    if host in {"localhost", "metadata.google.internal"} or host not in ALLOWED_BOT_HOSTS:
        return False, f"blocked non-allowlisted host: {host}"
    return True, "allow"


if __name__ == "__main__":
    weak_csp = "default-src 'self'; script-src 'self' 'unsafe-inline'; img-src *"
    weak_issues = audit_csp_header(weak_csp)
    assert len(weak_issues) >= 4
    print("[PASS weak CSP detected]", weak_issues)

    strong_csp = (
        "default-src 'none'; script-src 'nonce-abc123xyz' 'strict-dynamic'; "
        "img-src 'self'; connect-src 'self'; form-action 'self'; "
        "base-uri 'none'; object-src 'none'; require-trusted-types-for 'script'"
    )
    assert audit_csp_header(strong_csp) == []
    print("[PASS strong CSP validated]")

    for bad_url in (
        "javascript:alert(1)",
        "http://127.0.0.1:6379/",
        "http://169.254.169.254/latest/meta-data/",
        "https://attacker.example/trap.html",
    ):
        ok, reason = validate_bot_target_url(bad_url)
        assert not ok, f"expected block for {bad_url}"
        print(f"[PASS Bot URL] {bad_url} -> {reason}")

    assert validate_bot_target_url("https://app.example.com/ticket/42")[0] is True

验证与判定标准 ​

验证项预期加固表现阻断失败标志
HTML/SVG/MathML 净化<svg><animate>、<math><mtext> 及事件处理器全部被剥离或纯文本编码浏览器触发脚本执行或 onerror 访问外域
CSP 与外带通道无 nonce 脚本、connect-src、img-src、form-action 外域访问均被 CSP 拦截Listener 收到 sendBeacon/Image/form 任意一种外带请求
DOM Clobbering<form id=config> 等标记元素属性加前缀,不影响业务全局对象取值全局配置、apiBase 或 redirect_uri 被表单/锚点标签覆盖
Bot 会话与网络隔离Cookie 不可通过 document.cookie 读取;非白名单 URL 与 127.0.0.1 提交直接拒绝Bot 携带高权状态访问外部域名或返回内网端口探测时间差

MCP 工具映射 ​

防御环节MCP 工具说明
防御知识路由kb_router按 xss、csp、trusted types、dom clobbering、admin bot 搜索方案
读取攻击链路kb_read_file对照 ctf-website/07-client/admin-bot-xss 与 18-cors-csp-advanced/01-csp-bypass 校验缺口
浏览目录索引kb_catalog列出 07-client 与 18-cors-csp-advanced 防御条目
响应头核验http_probe切换到 red 或 both 轨道后检查响应头 CSP、HttpOnly、SameSite 属性

Apache-2.0 · For authorized learning and defensive research only · Disclaimer