网站跳转漏洞最直接的后果不是页面报错,而是你的域名权重被恶意消耗,甚至被搜索引擎判定为“恶意跳转站点”而直接降权或拉黑。攻击者利用未校验的跳转参数,把正常URL变成钓鱼页、博彩页、色情页的跳板。搜索引擎爬虫抓取到这些异常跳转后,会认为网站内容失控,轻则单页不收录,重则整站安全标记。很多站长发现排名莫名下滑,查日志才看到大量带外部域名的302请求,这就是跳转漏洞被利用的典型特征。

先明确一个概念:跳转漏洞不是“能被打开”就叫漏洞,而是“跳转目标可被任意控制”。凡是URL里带redirect、url、next、return、target、goto等参数,并且服务端或前端直接把这个参数值拼进跳转逻辑,就存在被利用的可能。攻击者会把恶意地址编码后塞进参数,普通用户看到的是你的域名开头,点击后却被带到完全无关的站点。搜索引擎在渲染页面时同样会跟随跳转,一旦识别到跳转链指向低质内容,你的页面就会背上“恶意导流”的标签。

先确认你的站有没有这类参数

检查所有涉及跳转的功能:登录后回跳、退出后回跳、支付完成回跳、下载前跳转、广告外链中转、移动端适配跳转。凡是URL中出现类似“?url=https://xxx.com”或“?redirect=aHR0cHM6Ly9leGFtcGxlLmNvbQ==”的结构,都要重点排查。不要只看前端代码,很多跳转是在服务端处理,前端看不到明显迹象。可以用浏览器开发者工具观察网络请求,找到返回301或302的接口,再看响应头里的Location字段是否受请求参数影响。

修复核心:白名单校验,而不是黑名单过滤

黑名单过滤是最容易踩坑的做法。有人觉得把“javascript:”“data:”“vbscript:”这些危险协议过滤掉就安全了,实际上攻击者可以用双重编码、大小写混合、协议相对地址“//evil.com”、反斜杠“\evil.com”等手法绕过。正确的做法是只允许跳转到站内相对路径,或者预先定义好的白名单域名列表。如果业务确实需要跳转到外部域名,必须做严格匹配,不能做“包含”判断。例如白名单里写“example.com”,攻击者用“example.com.evil.com”就能骗过包含判断。

服务端跳转修复示例

假设你的网站用PHP处理跳转,原始代码可能是这样:

$url = $_GET['url'];
header('Location: ' . $url);
exit;

这种写法等于把跳转控制权完全交给用户输入。修复后应当先判断目标是否站内路径:

$url = $_GET['url'] ?? '';
// 只允许以单斜杠开头的相对路径,且不允许双斜杠开头
if (preg_match('/^/(?!\/)/', $url)) {
    header('Location: ' . $url);
    exit;
} else {
    header('Location: /');
    exit;
}

如果必须支持外部域名跳转,就用白名单数组精确匹配:

$allowed_domains = ['partner1.com', 'partner2.com'];
$url = $_GET['url'] ?? '';
$host = parse_url($url, PHP_URL_HOST);
if ($host && in_array($host, $allowed_domains, true)) {
    header('Location: ' . $url);
    exit;
} else {
    header('Location: /');
    exit;
}

注意parse_url对畸形URL可能返回false,所以要先判断$host是否存在。另外要强制协议为https,避免http降级劫持。更稳妥的做法是把外部跳转地址存在服务端数据库或配置里,URL参数只传一个标识ID,前端完全不接触真实目标地址。

前端跳转同样要处理

有些站点用JavaScript读取URL参数后执行跳转,例如:

var url = getQueryString('url');
window.location.href = url;

这种写法连服务端日志都留不下,攻击者直接构造“https://你的域名/page?url=https://恶意域名”就能利用。前端修复必须做两层:第一层是判断目标是否站内地址,第二层是限制跳转协议。更推荐的做法是前端只跳转到站内中转页,由中转页的服务端逻辑做白名单校验后再输出真实跳转。

var url = getQueryString('url');
// 只允许站内相对路径
if (url && url.indexOf('/') === 0 && url.indexOf('//') !== 0) {
    window.location.href = url;
} else {
    window.location.href = '/';
}

但前端校验可以被绕过,攻击者直接调用接口或者构造表单提交,所以最终安全底线必须在服务端。前端修复只是减少普通用户误触发的概率,不能作为唯一防线。

搜索引擎视角的额外风险

跳转漏洞被利用后,搜索引擎爬虫会从你的页面出发,经过302跳转进入垃圾站点。如果爬虫频繁遇到这种情况,会把你的域名与垃圾站点建立关联。更隐蔽的玩法是攻击者用你的跳转链接做“链轮中转”,把权重传递给自己的站点。你查外链时看不到直接链接,但爬虫抓取路径里你的域名成了中间跳板。这种权重流失很难通过普通外链工具发现,只能靠服务器日志分析。

还有一个容易被忽略的点:跳转参数如果出现在被收录的URL里,搜索引擎可能把带参数的版本也建立索引。当参数值被替换成恶意地址时,搜索结果里展示的虽然是你的域名,点击后却去了别处。用户会认为是你的网站被黑,信任度直接崩塌。所以修复漏洞后,还要用robots规则或canonical标签处理带跳转参数的URL,避免它们被单独收录。

日志监控与长期维护

修复完代码不等于结束。你需要持续监控服务器日志,重点看两类请求:一是带跳转参数且参数值包含外部域名的请求,二是短时间内大量不同IP访问同一个跳转URL的情况。前者说明还有人在尝试利用漏洞,后者可能是攻击者在测试你的修复是否生效。如果发现大量302响应指向外部域名,要立刻检查是否还有未修复的入口。

建议在跳转逻辑里增加日志记录,把目标地址、来源IP、Referer、时间戳写入单独日志文件。这样即使事后发现问题,也能追溯攻击路径。对于已经收录的恶意跳转URL,可以在服务器端对已知恶意参数值直接返回404或410状态码,加速搜索引擎清除这些坏索引。

特殊情况:广告中转页和短链服务

如果你的网站本身就需要做外链中转,比如广告落地页跳转、下载链接统计,那么跳转参数是业务刚需,不能简单禁用。这种情况下要把中转页做成独立子域名或独立目录,与主站内容区隔离。同时给中转页加上noindex标签,避免中转URL被搜索引擎索引。中转逻辑必须做签名校验,例如把目标地址加盐后生成签名参数,服务端验证签名通过才执行跳转,防止攻击者篡改目标地址。

签名校验的做法示例:

// 生成跳转链接时
$target = 'https://partner.com/landing';
$secret = 'your-secret-key';
$sign = hash_hmac('sha256', $target, $secret);
$jump_url = '/go?target=' . urlencode($target) . '&sign=' . $sign;

// 跳转处理时
$target = $_GET['target'] ?? '';
$sign = $_GET['sign'] ?? '';
$expected = hash_hmac('sha256', $target, $secret);
if (hash_equals($expected, $sign)) {
    // 再做白名单域名校验
    header('Location: ' . $target);
} else {
    header('HTTP/1.1 403 Forbidden');
}

这样即使攻击者构造了target参数,没有正确签名也无法通过校验。签名密钥不要写在前端代码里,必须放在服务端配置文件或环境变量中。

修复后的验证方法

改完代码后不要只测正常跳转,要用攻击者视角验证。构造几个测试用例:双斜杠开头“//evil.com”、编码后的完整URL、带@符号的畸形地址、协议相对地址、大小写混合的javascript协议。确认这些输入要么被拒绝,要么被重定向到站内安全页面。同时检查HTTP响应码,确保非法跳转返回403或302到首页,而不是500错误。500错误虽然阻止了跳转,但可能暴露调试信息,也是安全隐患。

用curl命令可以快速验证:

curl -I "https://你的域名/redirect?url=//evil.com"

观察返回的Location头是否仍然指向evil.com。如果返回的Location是站内路径或者直接403,说明修复生效。多测几个参数名,因为同一个站可能在不同功能里用了不同的跳转参数名,比如登录用redirect,下载用target,退出用return,任何一个没改到都是漏洞入口。

跳转漏洞的修复成本不高,但放任不管的代价很大。它不像SQL注入那样直接拖库,而是持续消耗你的搜索信任度。很多站点被降权后找不到原因,最后发现是半年前的一个跳转参数被利用,垃圾页面已经通过跳转链爬了几个月。把白名单校验做扎实,把日志监控加上,把中转页隔离好,这个风险就能控制在很低的水平。