或是出于优化 SEO,或是出于加强网站体验,很多博客都给文章中的外部链接加上了个二次跳转,本博客也不例外。
比如说我在这插入一个 百度 的超链接,你点击访问后先会被跳转到本博客的跳转页面,一段动画后才会真的转至百度的首页。
看似合情合理,实则暗藏一个小漏洞!
情境分析
举个栗子,假设有个不怀好意的坏蛋,要传播一个不怀好意的网站 www.baidu.com (这里用百度做示范好了。。),通常这类网站一发到 QQ 里 QQ 就会有一个大大的红色叹号以示危险:
如果利用像本站的跳转功能,就能轻易地将链接“洗白”:
- https://mkblog.cn/go/?url=www.baidu.com
要是有不明真相的小白点击了,在本站的“跳转”下最终访问的就会是恶意的链接网址,无辜的链接跳转功能瞬间成了“帮凶”。
解决办法
那么如何防止这种情况的发生呢?
我的思路是这样的:
在跳转页面中,先用 PHP 的‘$_SERVER["HTTP_REFERER"]’函数获取来源链接,然后判断来源链接是不是属于本站,如果不是,再判断跳转链接,如果跳转链接也不属于本站,就给出一个提示:你将要访问的网站不属于本站范围,请谨慎访问。反之则正常跳转。
这里有个难点就是判断目标网址是不是属于本站的一个页面,也就是对“敌我”的一个判断。
比方说本站的网址是 http://mkblog.cn ,一些二级页面如 http://zb.mkblog.cn、https://mkblog.cn/about 也属于本站。而 http://www.baidu.com/?mkblog.cn 虽然包含“mkblog.cn”,访问后它却是百度的首页。因此不能简单粗暴的通过判断网址中是否存在 “mkblog.cn” 来得出结论。
仔细的观察域名的结构你就会发现只要是属于 mkblog.cn 的网址一定会满足以下规律:
- 如果 mkblog.cn 的前面有内容,那么一定不会有“?”出现,比如说 abc?.mkblog.cn 是不存在的
- 如果 mkblog.cn 的后面还跟有内容,那么一定是“/”,或“?”,比如说 mkblog.cn/123 或 mkblog.cn?from=123
利用以上两个规律写了个判断函数如下:
- /**
- * 判断是不是自己的域名
- * @param $domain 要进行判断的域名
- * @param $my 自己的域名
- * @return 对比结果
- */
- function isMyDomain($domain, $my) {
- preg_match('/([^\?]*)/i', $domain, $match);
- if(isset($match[1])) $domain = $match[1];
- preg_match('/([\w-]*\.[\w-]*)\/.*/i', $domain.'/', $match);
- if(isset($match[1]) && $match[1] == $my) return true;
- return false;
- }
最终代码
按照以上思路完成整个跳转页面的编写后,测试了一下效果非常不错:只要是从本站点开的跳转网址,都能正常进行跳转,从而第三方打开的跳转,则会弹出提示。
最终完整的跳转页面源代码如下:(代码中的判断思路肯定不唯一,如果您有更好的判断方式,欢迎在下方留言交流!)
效果演示
还是以百度为例,如果你 点击 https://mkblog.cn/go/?url=www.baidu.com 访问,直接就跳转了,如果你手动复制这个跳转网址再粘贴到浏览器访问,则会弹出提示。
如果是本站的站内链接,如 https://mkblog.cn/go/?url=zb.mkblog.cn 无论以何种方式打开都是直接跳转。
2017-3-13更新
经过评论区的各位提醒(感谢 @懿古今 @Tokin @a 指出问题),发现之前的版本存在 在微信中无法跳转、在部分浏览器中无法关闭页面、开启了https的站点无法获取HTTP_REFERER等情况。
现修复了这些问题的一部分。部分浏览器因为自身就屏蔽了关闭页面的js函数,所以还是会照成无法关闭页面的情况。(尽力了...)
至于 https 无法获取 HTTP_REFERER 的情况,现在我的站点都没有加 https ,不好测试,百度了一下也找不到好的解决方案,暂时还没有解决方案……
更新后的代码如下
- <?php
- /**
- * 带有来路验证和跳转提示功能的跳转页面
- * @auth 孟坤博客
- * @authUrl http://mkblog.cn
- * @data 2017/3/13
- * @url https://mkblog.cn/701
- */
- // 请将这里的网址改为自己的(顶级)域名地址
- $myDomain = 'mkblog.cn';
- // 这里用正则提取 $_SERVER["QUERY_STRING"] 而不是直接 get url
- // 是因为如果链接中自身带有 GET 参数则会导致获取不完整
- preg_match('/url=(.*)/i', $_SERVER["QUERY_STRING"], $jumpUrl);
- // 如果没获取到跳转链接,直接跳回首页
- if(!isset($jumpUrl[1])) {
- header("location:/");
- exit();
- }
- $jumpUrl = $jumpUrl[1];
- // 判断是否包含 http:// 头,如果没有则加上
- preg_match('/(http|https):\/\//', $jumpUrl, $matches);
- $url = $matches? $jumpUrl: 'http://'. $jumpUrl;
- // 判断网址是否完整
- preg_match('/[\w-]*\.[\w-]*/i', $url, $matche);
- // 是否需要给出跳转提示
- $echoTips = false;
- if($matche){
- // 如果是本站的链接,不展示动画直接跳转
- if(isMyDomain($url, $myDomain)) {
- header("location:{$url}");
- exit(); // 后续操作不再执行
- }
- $title = '页面加载中,请稍候...';
- $fromUrl = isset($_SERVER["HTTP_REFERER"])? $_SERVER["HTTP_REFERER"]: ''; // 获取来源url
- // 如果来源和跳转后的地址都不是本站,那么就要给出提示
- if(!isMyDomain($fromUrl, $myDomain)) {
- $echoTips = true;
- }
- } else { // 网址参数不完整
- $url = '/';
- $title = '参数错误,正在返回首页...';
- }
- /**
- * 判断是不是自己的域名
- * @param $domain 要进行判断的域名
- * @param $my 自己的域名
- * @return 对比结果
- */
- function isMyDomain($domain, $my) {
- preg_match('/([^\?]*)/i', $domain, $match);
- if(isset($match[1])) $domain = $match[1];
- preg_match('/([\w-]*\.[\w-]*)\/.*/i', $domain.'/', $match);
- if(isset($match[1]) && $match[1] == $my) return true;
- return false;
- }
- ?>
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
- <?php
- if($echoTips) {
- echo '<title>跳转提示</title>';
- } else {
- echo '<meta http-equiv="refresh" content="0;url='.$url.'">';
- echo '<title>'.$title.'</title>';
- }
- ?>
- <style>
- body{background:#fff;font-family:Microsoft Yahei;-webkit-animation:fadeIn 1s linear;animation:fadeIn 1s linear}
- @-webkit-keyframes fadeIn{from{opacity:0}
- to{opacity:1}
- }@keyframes fadeIn{from{opacity:0}
- to{opacity:1}
- }#circle{background-color:rgba(0,0,0,0);border:5px solid rgba(0,183,229,0.9);opacity:.9;border-right:5px solid rgba(0,0,0,0);border-left:5px solid rgba(0,0,0,0);border-radius:50px;box-shadow:0 0 35px #2187e7;width:50px;height:50px;margin:0 auto;position:fixed;left:30px;bottom:30px;-moz-animation:spinPulse 1s infinite ease-in-out;-webkit-animation:spinPulse 1s infinite ease-in-out;-o-animation:spinPulse 1s infinite ease-in-out;-ms-animation:spinPulse 1s infinite ease-in-out}
- #circle1{background-color:rgba(0,0,0,0);border:5px solid rgba(0,183,229,0.9);opacity:.9;border-left:5px solid rgba(0,0,0,0);border-right:5px solid rgba(0,0,0,0);border-radius:50px;box-shadow:0 0 15px #2187e7;width:30px;height:30px;margin:0 auto;position:fixed;left:40px;bottom:40px;-moz-animation:spinoffPulse 1s infinite linear;-webkit-animation:spinoffPulse 1s infinite linear;-o-animation:spinoffPulse 1s infinite linear;-ms-animation:spinoffPulse 1s infinite linear}
- @-webkit-keyframes spinPulse{0%{-webkit-transform:rotate(160deg);opacity:0;box-shadow:0 0 1px #505050}
- 50%{-webkit-transform:rotate(145deg);opacity:1}
- 100%{-webkit-transform:rotate(-320deg);opacity:0}
- }@-webkit-keyframes spinoffPulse{0%{-webkit-transform:rotate(0deg)}
- 100%{-webkit-transform:rotate(360deg)}
- }#loading-text{position:fixed;left:110px;bottom:35px;color:#736D6D}
- @media screen and (max-width:600px){#circle,#circle1{left:0;right:0;top:0;bottom:0}
- #circle{margin:120px auto}
- #circle1{margin:130px auto}
- #loading-text{display:block;text-align:center;margin-top:220px;position:static;margin-left:10px}
- }
- .warning{max-width: 500px;margin: 20px auto;}
- .wtitle {font-size: 22px;color: #d68300;}
- .wurl {overflow: hidden;text-overflow: ellipsis;white-space: nowrap;color: #827777;}
- .btn {display: inline-block;line-height: 20px;cursor: pointer;border: 1px solid #A9A6A6;padding: 6px 10px;font-size: 14px;text-decoration: none;}
- .btn-green {color: #fff;background-color: #238aca;border: 1px solid #238aca;}
- .btn:hover {background-color: #A9A6A6;border: 1px solid #A9A6A6;color: #fff;}
- </style>
- </head>
- <body>
- <?php if($echoTips) { ?>
- <div class="warning">
- <p class="wtitle">您将要访问:</p>
- <p class="wurl" title="<?php echo $url;?>"><?php echo $url;?></p>
- <p>该网站不属于孟坤博客,我们无法确认该网页是否安全,它可能包含未知的安全隐患。</p>
- <a class="btn btn-green" href="<?php echo $url;?>" rel="nofollow">继续访问</a>
- <span class="btn" onclick="closePage()">关闭网页</span>
- </div>
- <script>
- function closePage() {
- // 通用窗口关闭
- window.opener=null;
- window.open('','_self');
- window.close();
- // 微信浏览器关闭
- WeixinJSBridge.call('closeWindow');
- }
- </script>
- <?php } else { ?>
- <div id="circle"></div>
- <div id="circle1"></div>
- <p id="loading-text">页面加载中,请稍候...</p>
- <?php } ?>
- </body>
- </html>
本文作者为mengkun,转载请注明。
站长能不能把最新代码发布上来[托腮]
既然是防止滥用跳转,就直接不给跳转,跳回本站算了!
或者在跳转页写一个本链接被滥用,请返回本站查看!
https://sina.lt/ 这个短网址 跳转提示 怎么实现的?
在php里多增加几个安全的网站(网址),比如常见的百度网盘什么的,让他默认跳转不提示更好
请问一下 你的网站绿标怎么申请的。我也像申请个 谢谢!
如果要增加md5呢aHR0cDovL3d3dy53dWFpd2FuZy5jbg== 这样的
@会飞的鱼第一,这不叫 MD5,第二,这叫 base64,第三,使用 base64 的形式完全没必要……[斜眼笑]
判断像 a.com.cn 这种奇葩的根域名还是有点问题[aru_59]
@Cardinal这种域名的判断确实是个问题[aru_2]
如何解决外链视频的问题呢
插入视频 例如腾讯视频 也会被拦截到GO页面
@养鸽人在function.php里的go跳转正则里加入视频格式。MP4..........
isMyDomain 函数为啥不用 parse_url($url)['host'] 来取 URL 中的域名呢?[疑问]
@Wildlife用 parse_url 也行![aru_53] 不过还是要处理一下二级域名的情况
这个是个好东西,正好我博客没弄啦!
为什么不能用呢?老大。显示404:http://www.yeruofeng.club/go/?url=https://mkblog.cn/635/
文件不就是Nana/template-go.php 这个么?求帮助!
@空城需新建页面,模板选Go跳转页面,标题任意,别名为go
@mengkun怎么新建页面呢?我在文件夹里创建了一个GO的文件不管用。还有在创了一个go的文件里面放了此文件还是错误。。。求教
@空城http://t.cn/RT0q2L3
我网站也有类似的代码,我采用的是标记的方式,一个md5码一个网址,入库或者写缓存,没有找到数据就不跳转,靠来路判断是不靠谱的,伪造来路手段不少,而且判断来路禁止了其他方式传播,但标记的数据在可控范围内,所以我采用的是一码一链接。。
请问我想直接用http://abc.com/?http://www.baidu.com 这样的/?模式直接跳转应该怎么写代码?
在一个偶然的地方看到了这个博客,能学到的东西和看到的东西太多了!感谢,不过这东西怎么用!放在核心文件当中嘛
@章鱼哥替换掉之前的跳转页面模板即可
@mengkun没有,哈哈,都是直接跳转的!不过,你好快呐!刚刚评论就回复了!
@章鱼哥直接跳转就没必要用这个了 [wb_二哈] 我的博客有评论提醒的,所以一有新评论立刻就知道了
@mengkun我也想加一个!
@章鱼哥这个功能似乎是wordpress自带的啊……
@mengkun好吧,我去看看,谢谢了!不过,我不懂WORDPRESS
我觉得那个关闭页面可以改成返回,直接返回到打开页面。这样就可以不用考虑浏览器适配问题了
@萝卜是个不错的建议 [强]
@mengkun我看很多站都是这么做的,只有继续访问和返回。感觉很不错,望采纳
微信,QQ里面直接输网址,点继续访问和关闭网页都没效果
@a感谢反馈!刚测试了一下,微信中确实点继续访问会没反应 [衰] ,待会看看有没有办法解决……
HTTP_REFERER只能获取http,不能获取https
@Tokin谢谢赐教!没接触过 https, 又"乡巴佬"了一回 [流汗]
思路不错,很给力。测试的时候点击关闭网页好像没有效果,360浏览器测试
@懿古今多谢提醒!刚百度了一下,window.close() 的浏览器兼容确实不好。改成 window.open("","_self").close() 应该就可以了
这个方法可行,用上了。
就是喜欢看你博客!
自家的网站就直接跳转吧http://mkblog.cn/go/?url=http://www.mengkun.icoc.cc/
@瀚宇现在新的官网地址是 www.mkblog.cn ,很久不做pc端的软件,都没有去维护了 [偷笑]
@瀚宇老大。 为什么我把你的代码复制过来 到我的网站就不可以用了
对我看过有人用接口就像你说的这样用来洗白红的域名
@266277我之前用免费空间建站的时候就这么干过,因为免费空间送的二级域名一般都会报毒…… [呲牙]