少写一个 else,我的外链跳转页成了黑产眼中的“香饽饽”?重构一个安全的外链跳转页(附 PHP 源码)
“CV工程师”的小白迷之自信
时光倒流回好几年前,那时我的 PHP 和 JS 水平还停留在相当小白的阶段(虽然现在也还是业余),用现在流行的话说,就是一名标准的 “CV 程序员”(Ctrl+C / Ctrl+V)。
当时看到一些 SEO 教程说,不要直接引用外链,不然网站权重会丢失,不利于SEO优化云云,虽然那时候博客一个月的访问量加起来都没现在一天多,我还是决定搞一个外链跳转页,实现外链SEO优化。那时候的我,写代码基本靠搜。我当时在网上东拼西凑,这里复制一段 PHP 代码,那里又找来一段 JS 脚本,缝缝补补一通 Ctrl+C / Ctrl+V 算是把功能跑通了。

那时候的我觉得自己做得挺周全:
- 既有「关键词过滤」:在后端的 PHP 里加了正则,拦截类似
eval、base64这种敏感词,防止有人注入代码。 - 还有「来源页检查」:在前端的 JS 里写了判断,判断访问来源是不是我的博客域名,如果不是就跳回首页。
看着这套“组合拳”,我心想:“这下稳了,既防注入又防盗链,妥妥的。” 这一用,就是好几年。
我变成了黑产眼中的“香饽饽”:遭遇 Open Redirect 漏洞扫描
部署后的头几年,一切风平浪静,看起来跳转页在忠实的工作着。直到大前年开始,因为《本地部署AI文生图工具 SD-webui 生成NSFW图》部署教程被广泛引用,博客的流量和热度突然上去了。那小半年的时间里,每日新访客(仅仅是新访客就)能稳定在 4位数,高质量反链好几个。在 Google 和 Bing 眼中,我的博客权重逐渐变高。
于是乎,树大招风,我的外链跳转页被盯上了。
最开始的端倪,是 Google Search Console 发来的“提示”。网域出现了大量“未编入索引”的提示,我点进去一看,全是 /goto/?url=… 后面跟一长串乱七八糟的垃圾网站链接(赌博、色情、灰产,应有尽有)。

紧接着,防火墙(WAF)开始频繁报警。日志显示,有大量的请求携带着奇怪的参数试图经过我的跳转页做XSS或者SQL注入。这是典型的 Open Redirect(开放重定向)漏洞利用尝试。
佛系站长的机械抵抗
面对这些攻击,当时的我虽然觉得烦,但并没有意识到问题的严重性。毕竟博客前台看着没啥异常,服务器也没崩,搜索引擎也没收录这些奇奇怪怪的跳转。
于是,一向 “佛系” 的我采用了最机械的应对方式:
- WAF 堵截:我在防火墙里加了几条规则,只要 URL 参数里包含某些特征,就直接拦截。
- GSC 移除:对于 Google 收录的那些垃圾跳转链接,我直接使用 Google 的移除工具申请删除。

“反正也没造成什么实质性的破坏,能拦就拦,拦不住貌似也没啥。” 就这样,我拖着这个隐患,得过且过地又混了两年多。
形同虚设的“来源检查”:一段被覆盖的 JS 逻辑
直到最近,那种久违的“折腾之魂”突然死灰复燃。趁着手里有干劲,我决定把这个陈年老页面彻底重构一下。
当我打开那个尘封已久的 index.php,仔细审视当年的代码时,冷汗下来了。不看不知道,一看吓一跳——当年我所谓的“安全措施”,简直就是“千疮百孔”,甚至是在对黑灰产说着“欢迎光临”,代码幼稚的简直想穿回去抽自己的嘴巴子。
当年写下(复制来)的防止非本站使用跳转页的代码是这样的
{
//禁止其他网站使用我们的跳转页面
// 第一步:获取我们自己的域名
var MyHOST = new RegExp("<?php echo _SERVER['HTTP_HOST']; ?>");
// 第二步:判断来源
if (!MyHOST.test(document.referrer)) {
// 第三步:如果来源不对,准备跳转回首页
location.href="https://" + MyHOST;
}
// 第四步:正常执行跳转
location.href="<?php echourl;?>";
}
看出问题来没?我感觉稍微有点开发经验的朋友都看出来了,因为跳转逻辑被覆盖了!!
我当年知道 JavaScript 是按顺序向下执行的,所以我想当然的认为当代码执行了location.href="https://" + MyHOST;之后,非法访问就会被跳转到我的首页了,后面的location.href="<?php echo $url;?>";不会被执行。
但实际上,修改(赋值) location.href 后代码其实会继续执行下去的,实际执行时是下边这样的过程
- 一个非法的来源,进入 if 了。浏览器接到指令:“准备跳转回首页”。
- 毫秒级的时间内,代码会继续往下跑,执行到了下一行。浏览器接到新指令:“准备去目标外链”。
- 后一条指令覆盖了前一条指令,浏览器会听从最后一句代码的指挥。
- 结果:无论来源是否合法,浏览器都会乖乖跳转到 $url(目标网站)。
- 纯纯拦截了个寂寞

当年的我犯了初学者最常见的认知错觉,是把 JavaScript 的 location.href 当成了 PHP 里的 header('Location: ...');了,殊不知在浏览器眼里,这只是一次变量赋值。(PS:其实PHP里这样写也是错的,后面需要写exit;,不然可能用户浏览器已经跳走了,但服务器还在空跑)
浏览器是单线程执行 JS 的。当它读到我的第一次赋值时,它在心里记下:“哦,待会儿脚本跑完了我要去首页”。但是!脚本还没跑完呢,它必须继续往下跑。 紧接着它读到了第二次赋值:“哦,不对,他改主意了,待会儿脚本跑完了,让我去外链”。 后面的赋值覆盖了前面的赋值。
就像我告诉网约车司机‘去机场’,结果还没等司机踩油门,我又补了一句‘去火车站’。那司机肯定听最后一句啊!缺少一个 else,让我的防御代码变成了一句废话。
也就是说,这里正确的写法应该写成
var MyHOST = new RegExp("<?php echo _SERVER['HTTP_HOST']; ?>");
if (!MyHOST.test(document.referrer)) {
location.href = "https://" + MyHOST;
} else {
location.href = "<?php echourl;?>";
}
亦或者封装成一个函数用return打断函数继续执行也可以
var MyHOST = new RegExp("<?php echo _SERVER['HTTP_HOST']; ?>");
function CheckHOST() {
if (!MyHOST.test(document.referrer)) {
location.href = "https://" + MyHOST;
return; // <--- 让函数立即停止
}
location.href = "<?php echourl;?>";
}
顺带一提,这个错误的代码至今仍在谷歌搜索结果的前五位,而且被多个外链跳转页所使用。😂
拒绝漏洞:使用 PHP filter_var 重构安全跳转页
痛定思痛,我彻底抛弃了原来的代码,基于 PHP 服务端重写了整个逻辑。
为什么抛弃使用 JS 的检查逻辑
原因很简单,正确的 JS 代码当然可以在跳转被恶意利用时拉回用户,但这无法阻拦黑产的自动化漏洞扫描。
扫描漏洞用的爬虫、脚本(Python、Curl 、Go 等)根本就不执行 JS 的!它们只看 HTTP 响应头和 HTML 里的链接。 在扫描器眼里,我的旧代码压根没有那个 if 判断,他直接看到了最后的跳转链接。扫描器只会给我的各种路径去发?url=http://www.baidu.com之类的命令遍历尝试,看会不会触发跳转,只要触发跳转到百度的首页,就标记为“存在 Open Redirect 漏洞”,自动存入“可用资产库”。于是就会被拿来做跳转了,至于实际环境访问时能不能完成跳转,不讲究的黑产并不会去核实。只是因为有 JS 的存在,实际用户访问时会被拦截罢了。
从 JS 到 PHP:真正的来源检查
现在来源检查在服务器端完成,不依赖客户端。直接在 PHP 顶部加入了核心校验:
// 获取来源
referer = isset(_SERVER['HTTP_REFERER']) ? _SERVER['HTTP_REFERER'] : '';host = parse_url(referer, PHP_URL_HOST);
// 只有从我自己域名点出来的链接,才放行
if (!in_array(host, ['tjsky.net', 'www.tjsky.net'])) {
header('HTTP/1.1 403 Forbidden');
die('非法请求:禁止直接访问或盗链。');
}
- 直接 403:即使被利用了,正经的爬虫也能发现报了“403 拒绝请求”
- 不依赖 JS :彻底杜绝了扫描器把这里“误判”为开放重定向漏洞的可能。
放弃手搓正则,拥抱 filter_var:不再自己造轮子
当年真是的小白菜的常见状态,又菜又感觉自己强,当年的 URL 合法性检查是自己手搓的
strpos(_SERVER['REQUEST_URI'], "eval(") ||
strpos(_SERVER['REQUEST_URI'], "base64")||
strpos(_SERVER['REQUEST_URI'], "127.0.0.1")||
……
//省略其他过滤语句t_url = preg_replace('/^url=(.*)/i','1',_SERVER["QUERY_STRING"]);
//判断非空
if(!empty(t_url)) {
//判断取值是否是base64
if (t_url == base64_encode(base64_decode(t_url))) {
t_url = base64_decode(t_url);
}
我当时防御漏洞的逻辑很直观:黑客可能想干嘛,我就拦什么。黑客想传 eval,我就在代码里搜 eval;黑客想传 base64,我就搜 base64。这在安全领域叫“黑名单防御”,但其实:
- 这本来不应该是跳转页该做的事,这种事情应该交给更前边的更专业的WAF去做。
- 这种方式就像玩“打地鼠”,只要稍微换个姿势(比如利用 URL 编码或者空格绕过),我的之前正则就成了摆设。
- 而且逻辑本来就有bug,我为了不让部分跳转目标直接能看出来,加了可以将跳转目标 base64 化的机制,但问题是,我当年光想着检查要最优先进行了,忘了检查在解码base64的之前的话,又一次导致检查了个寂寞。
这次重构,因为使用了PHP跳转而不是前端跳转,所以改用 PHP 内置的filter_var来检查跳转 URL 的合法性,并且先解码,再检查。再配合FILTER_FLAG_NO_PRIV_RANGE之类的参数去防止对内网和私有 IP 做跳转。
//base64解码部分代码就不写了,只看filter_var部分的。
url_host = parse_url(final_url, PHP_URL_HOST);
//过滤本地主机
if (strtolower(url_host) === 'localhost') {
die('非法目标:你访问本地主机干啥。');
}
if (filter_var(url_host, FILTER_VALIDATE_IP)) {
//_PRIV_RANGE: 过滤 192.168.x.x, 10.x.x.x, 172.16.x.x 之类的大内网
//_RES_RANGE: 过滤 0.0.0.0, 127.0.0.1 等保留地址
if (!filter_var(url_host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
header('HTTP/1.1 403 Forbidden');
die('非法目标:你访问内网地址干啥。');
}
}
//确保链接格式符合 RFC 标准
if (!filter_var(final_url, FILTER_VALIDATE_URL)) die('目标链接格式错误。');
- 防止跳转内网和本地主机,作为一个外链跳转服务,没有任何正当理由需要指向一个内网或本地的 IP 地址。(这里只堵了IP,毕竟使用域名指向内网IP需要DNS配合,这利用难度就高了)
- 防止包含了空格、非法字符,错误的协议头:仅这一条就能这直接过滤掉了 90% 的低级扫描和恶意注入。
- 防止 XSS 脚本注入:以前构造
?url=javascript:alert(1)的话。在老代码里,这会被当作合法 URL 运行。而filter_var会识别出这不符合 Web 协议规范,直接在入口处将其掐死。 - 防止畸形参数:恶意利用时,很喜欢在参数里混入换行符或特殊的二进制字符来试图截断逻辑。这些东西在
filter_var的眼里都是不合法的它只认符合 RFC 标准的纯净 URL。
当然filter_var本质上是一个“格式校验器”,挡不住顶尖高手的定向渗透,比如传入一个格式完全标准,但指向内网某个邻居的数据库的域名。filter_var 会因为它符合 URL 规范而放行。这就是所谓的 SSRF(服务端请求伪造) 风险,这类深层次的逻辑漏洞,单靠一个函数是无法完全杜绝的。但面对互联网上每天成千上万次的自动化扫描和脚本攻击,它表现得比我那些个漏洞百出的正则要好太多了。放弃对“手搓代码”的执念,承认现成工具套件,尤其是安全、密码方向的现成轮子专业能力。现在的外链跳转页代码已经是一份真正的PHP安全跳转代码了。
更多细节修改:交互和 SEO 的多重升级

把原来的“静默X秒后跳转”改成了“倒计时卡片”:
- 使用 10秒倒计时:给访客足够的时间看清“您即将离开博客”的提示。
- 在页面内显示目标网址,看清到底要去哪里,确认目标网址安全。
- 加一个立即跳转的按钮,让不想等自动跳转的人可以立刻去要去的地方。
增加 htmlspecialchars 防止 XSS 攻击
因为现在的跳转页有一个跳转URL“预览框”。如果万一黑产绕过了前面的检查,把一段代码伪装成 URL 传了进来,直接打印在 HTML 里是非常危险的,搞不好就引发 XSS 攻击了。 htmlspecialchars会把所有的 <、>、"等特殊字符全部转义成 HTML 实体,让前端可以安全展示URL。
$display_url = htmlspecialchars($final_url, ENT_QUOTES, 'UTF-8');
增加 noindex, nofollo 标头
额外增加 X-Robots-Tag:直接在 HTTP 标头中加入 noindex, nofollow,直接告诉搜索引擎爬虫:“这个页面通通都别收录,权重别传递”。这比在 <head> 里写 <meta name="robots" content="noindex, nofollow" /> 更优先,爬虫不需要解析 HTML 就能看到,从而更有效保护博客的 SEO 资产。(当然meta也要加,毕竟某搜索引擎的爬虫不认这个响应头)
<?php
header('X-Robots-Tag: noindex, nofollow');
进阶玩法:使用 AES 加密隐藏真实链接
本来这次对外链跳转页的重构也就止步于上一步了,有些熟悉本站的人,估计已经发现新版跳转页已经上线运行了一段时间了。
我最近在爬取一个图片资源站时,发现对方有个很有意思的设计,这个图片站为了防止遍历本地路径实现快速抓取图片资源,他把站内的图片链接都AES加密了。我只能看到他图片都是类似主域名/img/?url=OVA2Q……HBIdz09-d这样的地址。如果不知道加密密钥,就无法生成正确的资源地址。
我一琢磨,虽然我的跳转页有个来源域名检测,防止非本站访问。但 HTTP Referer 伪造起来难度也不高。要是真有人拿我的跳转页去搞伪装钓鱼的话,他自然能伪造请求头的。
于是我决定做个“二次进化”:使用 AES 加密跳转地址。
不过这个图片站的 AES 加密方案还是不太完善,因为很容易就能发现,他所有图片的开头和结尾字符串都是固定的。很明显是个使用 ECB 模式(固定使用同一个密钥)做对称加密的结果。(我很轻松的就利用已知明文部分爆破出了他的密钥)
我打算更进一步使用 CBC 模式 (随机初始化向量)做加密,逻辑是:[ 随机生成的 16 字节 IV ] + [ AES 加密后的实际网址 ] 打包在一起再 Base64 编码。
这样服务器只要收到密文后,只需要先解码base64,截取前16个字节作为IV,用密钥和刚才拿到的IV,去解密后面的密文,就可以得到实际跳转地址。
$final_url = '';
// 尝试 AES 解密
$binary_data = base64_decode($input_code);
if ($binary_data && strlen($binary_data) > 16) {
$iv = substr($binary_data, 0, 16);
$ciphertext = substr($binary_data, 16);
// 使用预设的密钥解密
$decrypted = openssl_decrypt($ciphertext, AES_METHOD, AES_KEY, OPENSSL_RAW_DATA, $iv);
if ($decrypted && strpos($decrypted, '://') !== false) {
$final_url = $decrypted;
}
}
这样带来的好处是巨大的:
1. 动态密文:因为 IV 是随机的,同一个网址每次加密出来的字符串都不一样,无法通过简单的特征分析来破解。
2. 杜绝搬运:那些喜欢“采集”我文章的爬虫(说的就是你,CSDN),搬运过去后跳转链接会直接失效,倒逼他们必须手动处理。
3. 无数据库:不需要像短链外链方案,需要数据库存链接白名单,我只需要保存好密钥就可以,只要解密失败,就说明这不是我自己生成的链接。
4. 防伪造:取消明文跳转和基础base64链接跳转后,可以彻底堵死跳转页可被第三方利用的可能性。只要解密失败直接不跳了。
结语
咋说呢,当年作为“CV 工程师”省下的脑子,最后都变成了日志里的红字报警。
现在的跳转页,虽然功能没变,但它不再是一个过于容易被攻破的漏洞,而是一个安全、可控的流量出口。如果你也在维护类似的跳转功能,不妨也检查一下:你的跳转页的“安全检查”,是真的在检查,还是在自欺欺人?
📂 源码下载
跳转页的代码已经在Github开源啦(含详细注释,适用于 WordPress / Typecho / Z-Blog 等一切 PHP 站点):
👉 点击查看:安全的外链跳转页源码
前边这个外链的跳转就是使用AES加密后的跳转链接,只有到执行跳转时才会显示出真正的跳转目标。
AI辅助创作声明:跳转页源码经过 AI 增加详细注释(他还顺手优化了一行代码),流程图、头图是 AI 画的。
因为既然开源了需要提供一个生成加密链接的demo工具,但问题是我自己不用单独页方案,我也懒得自己新写一个,所以这个项目中的加密PHP也是让AI写的,我仅测试了可运行。


J.sky
2026-01-27 19:11
WP 对于新手部署确实友好,但是运维想要做到熟练,真得花些时间。
J.sky
2026-01-27 19:04
你俩都是真正的大佬。
J.sky
2026-01-27 17:20
膜拜了!
obaby
2026-01-27 17:08
这个属实专业了
去年夏天
2026-01-27 17:22
姐姐才是专业的,我这纯属业余水平😄