我的博客网站被克隆了!但是我只用了14行代码完成反击
2026 年 7 月 25 日,周六,早上 9:30。
我像往常一样打开搜索引擎,输入 lcrworld.xyz 自搜一下——看看收录有没有变化、排名涨了没。这是站长的日常,一种 职业病 好习惯😊。
结果这一搜,搜出事了。
搜索结果里除了我的 lcrworld.xyz,还赫然躺着一个 arvcbv.com。标题一样、描述一样、点进去一看——页面也一模一样。我新发的文章,他那边同步更新了。我换了个头图,他那边也跟着换了。

当时我的表情大概是这样的:◐⩊◑
一、恐慌:9:30 — 10:00
第一反应:服务器被黑了。
冷静分析一下,如果对方只是扒站(wget 下载前端页面),那文章内容应该是静态的、有延迟的。但 arvcbv.com 上的文章是实时的,我刚发的他就有。这意味着两种可能:
- 他入侵了我的服务器,直接读了我的数据库。
- 他在做某种实时代理。
不管是哪种,都得先确认服务器有没有被入侵。于是开始了全面取证。
先看 SSH 登录日志。这一看,血压直接上来了:
$ grep 'Failed password' /var/log/auth.log | wc -l
2847
$ grep 'Failed password' /var/log/auth.log | awk '{for(i=1;i<=NF;i++) if($i=="from") print $(i+1)}' | sort | uniq -c | sort -rn | head -5
596 91.92.40.50 # 保加利亚
503 27.71.230.31 # 越南
377 8.219.137.42 # 阿里云新加坡
146 1.255.171.167 # 韩国
96 39.97.229.27 # 阿里云杭州
来自 20 多个 IP 的 2800+ 次暴力破解,日夜不停地轰炸我的 22 端口。保加利亚的这位大哥尤其执着,596 次,平均一天 20 多次,比我打卡还规律。
然后检查了一下服务器的防护配置:
- fail2ban?没装。
- UFW 防火墙?没开。
- root 密码登录?开着。
- SSH 默认端口?22。
好家伙,这配置简直就是在门口挂了个牌子写着"欢迎光临,门没锁,钥匙在垫子下面"。2800 多次爆破没有一次被自动拦截,全靠密码本身扛着。(这段是AI骂我的时候写的wwww)
但密码能扛多久?迟早有一天被人撞开。想到这里后背一阵发凉。
但是——成功入侵了吗?
查完所有成功登录记录,松了一口气:
| 检查项 | 结果 |
|---|---|
| 成功登录 IP | 全部中国境内,和我的活动范围一致 |
| 后门 / 恶意进程 | 没有 |
| 异常 crontab | 没有 |
| SUID 提权文件 | 全是系统默认的 |
| LD_PRELOAD 注入 | 没有 |
| .bash_history | 干干净净,就几条正常命令 |
| 系统文件被篡改 | 查了 apt 历史,是 Ubuntu 自动安全更新 |
服务器没有被入侵。
好,排除了一种可能。但困惑反而更深了——
没入侵我的服务器,他是如何通过别的方式克隆我整个网站的?
我的源代码没泄露过,Git 仓库是私有的,数据库密码也没给任何人。一个完整的网站克隆——前端页面 + 后端 API + 实时数据同步——这总不能是简单的扒站工具干的吧?
带着这个疑问,开始对比两个网站的技术细节。
二、真相:10:00 — 10:30
真相大白的过程,比我想象的快得多,也简单得多。简单到让人想笑。
第一步:对比前端文件
arvcbv.com: assets/index-1K0S1bxy.js
lcrworld.xyz: assets/index-1K0S1bxy.js
Bundle hash 一模一样。
arvcbv.com 上这个文件的 last-modified 是 7 月 23 日——两天前。那天,对方只是跑了一行命令:
wget -r -k -p https://lcrworld.xyz/
把我的网站所有公开文件下载到了他的服务器上。HTML、JS、CSS、字体——这些东西本来就对全网公开,浏览器要渲染页面就必须能下载。任何人打开开发者工具的 Network 面板,都能把这些文件一个个保存下来。
他不需要我的源代码。编译后的产物就是公开的。SPA 网站编译后的前端代码本身就是完全公开的,和源代码等价(尤其是对有耐心的人来说)。
第二步:对比 API 响应头
前端文件能下载,但文章数据呢?arvcbv.com 上显示的是我数据库里的实时文章,这是怎么做到的?
对比两个网站的 API 响应头:
# arvcbv.com 的 API 响应头
Server: cloudflare
ETag: W/"3660-..." ← Cloudflare 生成的
# lcrworld.xyz 的 API 响应头
Server: nginx/1.30.3
arvcbv.com 的 Server 是 cloudflare,不是 nginx。说明它走的是 Cloudflare 的服务。而我直接请求 arvcbv.com/api/rss/articles,返回的是我数据库里的真实文章——格式、内容、时间戳完全一致。
真相只有一个:
他用 Cloudflare Worker 写了一个反向代理,把所有 /api/* 请求转发到了 https://lcrworld.xyz/api/*。
// Cloudflare Worker 伪代码,大概就这么几行
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname.startsWith('/api')) {
// 所有 API 请求转发到原站
return fetch('https://lcrworld.xyz' + url.pathname + url.search, request);
}
// 其他请求走本地静态文件
return fetch(request);
}
}
访客访问 arvcbv.com → Cloudflare Worker 把请求转发到我服务器 → 我的服务器忠实地返回文章数据。从服务器的角度看,这就是一个正常的 API 请求,它没有理由拒绝。
整个过程,对方只需要四步:
1. wget -r https://lcrworld.xyz/ ← 下载公开文件
2. 部署到自己服务器 / Cloudflare Pages
3. 写一个 Cloudflare Worker 做 API 代理
4. 绑定域名 arvcbv.com
没有任何一步需要入侵我的服务器。不需要 SSH 登录,不需要源代码,不需要数据库密码,不需要任何黑客技术。连脚本小子都算不上,这只是一个会写 Cloudflare Worker 的普通人就能做到的事。
这就是 SPA(单页应用)网站的天然弱点:前端代码完全公开 + API 可以被反向代理。任何 Vue/React/Angular 写的网站都可以用这个方法克隆。相当于在自己家门口装一面镜子,把对面屋里的画面实时反射过来。
三、反击:10:30 — 11:30
搞清楚原理之后,事情就简单了。既然他的 API 是代理我的服务器,那我在服务器端做校验,他的代理就会直接失效。
分两步走,先断图片,再断 API。
第一刀:Nginx 图片防盗链
arvcbv.com 的图片是热链我的服务器——HTML 里是相对路径 /uploads/xxx.jpg,通过 Cloudflare Worker 代理后实际从我服务器加载。等于他白嫖我的存储和带宽,给我刷 PV。
(我说怎么ICP备案期间,明明DNS解析都改到别的服务器了,网站访问就会REFUSED,每天的PV还是有几个呢)
在 Nginx 配置里加一段 valid_referers 校验:
location ~* \.(png|jpg|jpeg|gif|webp|avif|svg)$ {
valid_referers none blocked server_names
*.lcrworld.xyz lcrworld.xyz
~\.google\. ~\.bing\. ~\.baidu\. ~\.yandex\.;
if ($invalid_referer) {
return 403;
}
expires 30d;
}
效果:arvcbv.com 上所有图片瞬间全部裂图。访客看到的就是一片白板加裂图图标。原本漂漂亮亮的文章页,瞬间变成了 1998 年的纯文本网页。
但这只是开胃菜。图片断了只是皮外伤,文章内容还在,API 还在正常代理。真正的杀手锏在下面。
第二刀:Express Host 头校验 —— 致命一击
这一步是核心。
Cloudflare Worker 把请求转发到我服务器时,携带的 Host 头是 arvcbv.com——这是 HTTP 协议的基本行为,Worker 没法也不需要改这个头。那我只要在 Express 后端检查 Host 头,非本站请求一律拒绝就行了。
我在门口装了个门禁,刷我家的卡才让进。你拿着别人的卡来,门禁直接报警。😡
在 app.js 里 const app = express() 后面加一个中间件:
const ALLOWED_HOSTS = ['lcrworld.xyz', 'www.lcrworld.xyz', 'en.lcrworld.xyz', 'localhost', '127.0.0.1'];
const ALLOWED_ORIGINS = ['https://lcrworld.xyz', 'https://www.lcrworld.xyz', 'https://en.lcrworld.xyz'];
app.use((req, res, next) => {
if (process.env.NODE_ENV !== 'production') return next();
const host = (req.headers['x-forwarded-host'] || req.headers.host || '').toLowerCase().split(':')[0];
const origin = (req.headers['origin'] || '').toLowerCase();
const hostOk = ALLOWED_HOSTS.some(allowed => host === allowed || host.endsWith('.' + allowed));
const originOk = !origin || ALLOWED_ORIGINS.some(allowed => origin === allowed);
if (!hostOk || !originOk) {
console.warn(`[BLOCKED] Host=${host} Origin=${origin} Path=${req.path}`);
return res.status(403).json({ error: 'Unauthorized domain' });
}
next();
});
就这么十几行代码。pm2 restart blog-server,搞定。
原理很简单:Cloudflare Worker 转发请求时,Host 头是 arvcbv.com,不在白名单里 → 直接 403。他无法伪造 lcrworld.xyz 的 Host 头,因为域名不归他控制——除非他把我的 DNS 也劫持了,但那就是另一个故事了。
验证:一击毙命
中间件上线后,立即测试:
# 原站正常
$ curl -s -o /dev/null -w '%{http_code}' lcrworld.xyz/api/rss/articles
200 ✓
# 克隆站 - API
$ curl -s arvcbv.com/api/rss/articles
{"error":"Unauthorized domain"}
403 ✓
# 克隆站 - 首页
$ curl -s -o /dev/null -w '%{http_code}' arvcbv.com/
403 ✓
arvcbv.com 全站 403。
文章列表,403。文章内容,403。评论,403。图片,403。搜索,403。所有 API,全部 403。
访客打开 arvcbv.com,看到的是一个光秃秃的白屏,上面写着:
{"error":"Unauthorized domain"}。

他之前 wget -r 下载的所有静态文件、写的 Cloudflare Worker、配置的域名 DNS、申请的 SSL 证书——全部白干。一夜回到解放前。🤣
而且他没有任何办法绕过。Host 头是 HTTP 协议层面的东西,他改不了。除非他把我整个数据库也复制一份到他自己的服务器上——但那是"搬家"不是"克隆",而且他没有我的数据库。
PM2 日志里可以看到拦截记录:
[BLOCKED] Host=arvcbv.com Origin=https://arvcbv.com Path=/api/articles UA=Mozilla/5.0...
[BLOCKED] Host=arvcbv.com Origin= Path=/api/rss/articles UA=Mozilla/5.0...
每一行 BLOCKED 都是一次被拒绝的请求。想象一下他刷新自己网站看到白屏时的表情——大概和我早上 9:30 发现被克隆时的表情一样精彩。
从发现到封堵,两个小时。9:30 发现,11:30 收工。
四、关于报警
很多人遇到这种事第一反应是报警。说实话,我也认真考虑过,还做了完整的证据采集——服务器全量日志 534MB、20 多个攻击 IP 列表、完整的取证报告。但冷静分析下来:
SSH 暴力破解——没成功入侵,攻击源主要是境外 IP(保加利亚、越南、韩国),跨境执法几乎不可能。全网每台开放 22 端口的服务器每天都在被扫,这在安全圈叫"互联网背景噪音"。你报警说"有人敲了我家门 2800 次没敲开",警察大概会让你装个更好的锁。
网站克隆——属于侵犯著作权和不正当竞争,但刑事立案门槛高,需要证明实际经济损失。更实际的维权路径是向域名注册商投诉、向 Cloudflare 发 abuse report 举报其 Worker 侵权、发律师函要求下架。技术封堵比走法律程序直接得多,效果立竿见影。
当然,所有证据已经打包保存了。万一后面要走法律途径,证据链是完整的。先技术封堵止血,法律维权慢慢来。
五、教训
这次事件暴露了两个层面的问题。
安全防护层面——服务器裸奔太久了。fail2ban 没装、防火墙没开、root 密码登录开着、SSH 用默认 22 端口。2800 次暴力破解没有任何自动拦截机制。虽然这次没被入侵,但说实话,纯属运气好。密码是扛住了,但谁能保证下一次?已列入加固清单:
- 装 fail2ban:5 次失败封禁 24 小时
- 开 UFW 防火墙:只开 22/80/443
- 禁 root 密码登录,换 SSH 密钥认证
- 改 SSH 端口:别用 22
架构层面——SPA 网站的前端代码是完全公开的,API 可以被任意反向代理。这是所有 SPA 的通病,不是 bug,是 feature——浏览器需要能下载这些文件才能渲染页面,你没法不公开。但只要在 API 层做 Host 校验,克隆站就是一戳就破的纸老虎。
这就像是:你的商店橱窗是透明的(前端公开),谁都能看、谁都能拍照。但收银台(API)你可以验身份,不是你店里的人不卖。他站在自己店里拿对讲机帮你点单(反向代理),你只要在收银台说一句"只认本店会员",他的对讲机就废了。
六、最后
整个事件从发现到解决,两个小时。最终结果:
| 站点 | API | 首页 | 图片 |
|---|---|---|---|
| lcrworld.xyz(原站) | 200 ✓ | 200 ✓ | 200 ✓ |
| arvcbv.com(克隆站) | 403 ✗ | 403 ✗ | 403 ✗ |
克隆者花时间下载文件、写 Worker、配域名、申请证书,我花十几行代码就让这一切归零。
如果你也是站长,建议现在就去你的 API 加一个 Host 头校验。五分钟的事,但能让 99% 的克隆者白干一场。
毕竟,让坏人白干一场,是这个世界上为数不多的、纯粹的快乐。
全文完
评论 (0)