保护 Go 跳转,防止被恶意利用 | 珊瑚贝

或是出于优化 SEO,或是出于加强网站体验,很多博客都给文章中的外部链接加上了个二次跳转,本博客也不例外。

比如说我在这插入一个 百度 的超链接,你点击访问后先会被跳转到本博客的跳转页面,一段动画后才会真的转至百度的首页。

看似合情合理,实则暗藏一个小漏洞!

情境分析

举个栗子,假设有个不怀好意的坏蛋,要传播一个不怀好意的网站 www.baidu.com (这里用百度做示范好了。。),通常这类网站一发到 QQ 里 QQ 就会有一个大大的红色叹号以示危险:
保护 Go 跳转,防止被恶意利用

如果利用像本站的跳转功能,就能轻易地将链接“洗白”:

  1. https://mkblog.cn/go/?url=www.baidu.com

保护 Go 跳转,防止被恶意利用

要是有不明真相的小白点击了,在本站的“跳转”下最终访问的就会是恶意的链接网址,无辜的链接跳转功能瞬间成了“帮凶”。

解决办法

那么如何防止这种情况的发生呢?

我的思路是这样的:

在跳转页面中,先用 PHP 的‘$_SERVER[“HTTP_REFERER”]’函数获取来源链接,然后判断来源链接是不是属于本站,如果不是,再判断跳转链接,如果跳转链接也不属于本站,就给出一个提示:你将要访问的网站不属于本站范围,请谨慎访问。反之则正常跳转。
保护 Go 跳转,防止被恶意利用

这里有个难点就是判断目标网址是不是属于本站的一个页面,也就是对“敌我”的一个判断。

比方说本站的网址是 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

利用以上两个规律写了个判断函数如下:

  1. /**
  2.  * 判断是不是自己的域名
  3.  * @param $domain 要进行判断的域名
  4.  * @param $my 自己的域名
  5.  * @return 对比结果
  6.  */
  7. function isMyDomain($domain$my) {
  8.     preg_match(‘/([^\?]*)/i’, $domain$match);
  9.     if(isset($match[1])) $domain = $match[1];
  10.     preg_match(‘/([\w-]*\.[\w-]*)\/.*/i’, $domain.’/’, $match);
  11.     if(isset($match[1]) && $match[1] == $myreturn true;
  12.     return false;
  13. }

最终代码

按照以上思路完成整个跳转页面的编写后,测试了一下效果非常不错:只要是从本站点开的跳转网址,都能正常进行跳转,从而第三方打开的跳转,则会弹出提示。

最终完整的跳转页面源代码如下:(代码中的判断思路肯定不唯一,如果您有更好的判断方式,欢迎在下方留言交流!)

2017/3/3版存在一些BUG,仅供参考,已被折叠。新版请前往文末
  1. <?php
  2. /**
  3.  * 带有来路验证和跳转提示功能的跳转页面
  4.  * @auth 孟坤博客
  5.  * @authUrl http://mkblog.cn
  6.  * @data 2017/3/3
  7.  * @url https://mkblog.cn/701
  8.  */
  9. // 请将这里的网址改为自己的(顶级)域名地址
  10. $myDomain = ‘mkblog.cn’;
  11. // 这里用正则提取 $_SERVER[“QUERY_STRING”] 而不是直接 get url
  12. // 是因为如果链接中自身带有 GET 参数则会导致获取不完整
  13. preg_match(‘/url=(.*)/i’, $_SERVER[“QUERY_STRING”], $jumpUrl);
  14. // 如果没获取到跳转链接,直接跳回首页
  15. if(!isset($jumpUrl[1])) {
  16.     header(“location:/”);
  17.     exit();
  18. }
  19. $jumpUrl = $jumpUrl[1];
  20. // 判断是否包含 http:// 头,如果没有则加上
  21. preg_match(‘/(http|https):\/\//’, $jumpUrl, $matches);    
  22. $url = $matches$jumpUrl: ‘http://’. $jumpUrl;
  23. // 判断网址是否完整
  24. preg_match(‘/[\w-]*\.[\w-]*/i’, $url$matche);
  25. // 是否需要给出跳转提示
  26. $echoTips = false;
  27. if($matche){
  28.     // 如果是本站的链接,不展示动画直接跳转
  29.     if(isMyDomain($url$myDomain)) {
  30.         header(“location:{$url}”);
  31.         exit();    // 后续操作不再执行
  32.     }
  33.     $title = ‘页面加载中,请稍候…’;
  34.     $fromUrl = isset($_SERVER[“HTTP_REFERER”])? $_SERVER[“HTTP_REFERER”]: // 获取来源url
  35.     // 如果来源和跳转后的地址都不是本站,那么就要给出提示
  36.     if(!isMyDomain($fromUrl$myDomain)) {
  37.         $echoTips = true;
  38.     }
  39. else {    // 网址参数不完整
  40.     $url = ‘/’;
  41.     $title = ‘参数错误,正在返回首页…’;
  42. }
  43. /**
  44.  * 判断是不是自己的域名
  45.  * @param $domain 要进行判断的域名
  46.  * @param $my 自己的域名
  47.  * @return 对比结果
  48.  */
  49. function isMyDomain($domain$my) {
  50.     preg_match(‘/([^\?]*)/i’, $domain$match);
  51.     if(isset($match[1])) $domain = $match[1];
  52.     preg_match(‘/([\w-]*\.[\w-]*)\/.*/i’, $domain.’/’, $match);
  53.     if(isset($match[1]) && $match[1] == $myreturn true;
  54.     return false;
  55. }
  56. ?>
  57. <html>
  58. <head>
  59. <meta http-equiv=“Content-Type” content=“text/html; charset=UTF-8”>
  60. <meta http-equiv=“X-UA-Compatible” content=“IE=edge”>
  61. <meta name=“viewport” content=“width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no”>
  62. <?php
  63. if($echoTips) {
  64.     echo ‘<title>跳转提示</title>’;
  65. else {
  66.     echo ‘<meta http-equiv=“refresh” content=“0;url=’.$url.'”>’;
  67.     echo ‘<title>’.$title.'</title>’;
  68. }
  69. ?>
  70. <style>
  71. body{background:#fff;font-family:Microsoft Yahei;-webkit-animation:fadeIn 1s linear;animation:fadeIn 1s linear}
  72. @-webkit-keyframes fadeIn{from{opacity:0}
  73. to{opacity:1}
  74. }@keyframes fadeIn{from{opacity:0}
  75. to{opacity:1}
  76. }#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}
  77. #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}
  78. @-webkit-keyframes spinPulse{0%{-webkit-transform:rotate(160deg);opacity:0;box-shadow:0 0 1px #505050}
  79. 50%{-webkit-transform:rotate(145deg);opacity:1}
  80. 100%{-webkit-transform:rotate(-320deg);opacity:0}
  81. }@-webkit-keyframes spinoffPulse{0%{-webkit-transform:rotate(0deg)}
  82. 100%{-webkit-transform:rotate(360deg)}
  83. }#loading-text{position:fixed;left:110px;bottom:35px;color:#736D6D}
  84. @media screen and (max-width:600px){#circle,#circle1{left:0;right:0;top:0;bottom:0}
  85. #circle{margin:120px auto}
  86. #circle1{margin:130px auto}
  87. #loading-text{display:block;text-align:center;margin-top:220px;position:static;margin-left:10px}
  88. }
  89. .warning{max-width: 500px;margin: 20px auto;}
  90. .wtitle {font-size: 22px;color: #d68300;}
  91. .wurl {overflow: hidden;text-overflow: ellipsis;white-space: nowrap;color: #827777;}
  92. .btn {display: inline-block;line-height: 20px;cursor: pointer;border: 1px solid #A9A6A6;padding: 6px 10px;font-size: 14px;text-decoration: none;}
  93. .btn-green {color: #fff;background-color: #238aca;border: 1px solid #238aca;}
  94. .btn:hover {background-color: #A9A6A6;border: 1px solid #A9A6A6;color: #fff;}
  95. </style>
  96. </head>
  97. <body>
  98.     <?php if($echoTips) { ?>
  99.     <div class=“warning”>
  100.         <p class=“wtitle”>您将要访问:</p>
  101.         <p class=“wurl” title=“<?php echo $url;?>”><?php echo $url;?></p>
  102.         <p>该网站不属于孟坤博客,我们无法确认该网页是否安全,它可能包含未知的安全隐患。</p>
  103.         <span class=“btn btn-green” onclick=“go();”>继续访问</span>
  104.         <span class=“btn” onclick=“window.close();”>关闭网页</span>
  105.     </div>
  106.     <script>
  107.     function go() {
  108.         window.location.href = window.location.href;
  109.     }
  110.     </script>
  111.     <?php } else { ?>
  112.     <div id=“circle”></div>
  113.     <div id=“circle1”></div>
  114.     <p id=“loading-text”>页面加载中,请稍候…</p>
  115.     <?php } ?>
  116. </body>
  117. </html>

效果演示

还是以百度为例,如果你 点击 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 ,不好测试,百度了一下也找不到好的解决方案,暂时还没有解决方案……

更新后的代码如下

  1. <?php  
  2. /** 
  3.  * 带有来路验证和跳转提示功能的跳转页面 
  4.  * @auth 孟坤博客 
  5.  * @authUrl http://mkblog.cn 
  6.  * @data 2017/3/13 
  7.  * @url https://mkblog.cn/701 
  8.  */  
  9.   
  10. // 请将这里的网址改为自己的(顶级)域名地址  
  11. $myDomain = ‘mkblog.cn’;  
  12.   
  13. // 这里用正则提取 $_SERVER[“QUERY_STRING”] 而不是直接 get url  
  14. // 是因为如果链接中自身带有 GET 参数则会导致获取不完整  
  15. preg_match(‘/url=(.*)/i’, $_SERVER[“QUERY_STRING”], $jumpUrl);   
  16.   
  17. // 如果没获取到跳转链接,直接跳回首页  
  18. if(!isset($jumpUrl[1])) {  
  19.     header(“location:/”);  
  20.     exit();  
  21. }  
  22.   
  23. $jumpUrl = $jumpUrl[1];  
  24.   
  25. // 判断是否包含 http:// 头,如果没有则加上  
  26. preg_match(‘/(http|https):\/\//’, $jumpUrl, $matches);      
  27.   
  28. $url = $matches$jumpUrl: ‘http://’. $jumpUrl;  
  29.   
  30.   
  31. // 判断网址是否完整  
  32. preg_match(‘/[\w-]*\.[\w-]*/i’, $url$matche);      
  33.   
  34. // 是否需要给出跳转提示  
  35. $echoTips = false;  
  36.   
  37. if($matche){  
  38.     // 如果是本站的链接,不展示动画直接跳转  
  39.     if(isMyDomain($url$myDomain)) {  
  40.         header(“location:{$url}”);  
  41.         exit();    // 后续操作不再执行  
  42.     }  
  43.       
  44.     $title = ‘页面加载中,请稍候…’;  
  45.     $fromUrl = isset($_SERVER[“HTTP_REFERER”])? $_SERVER[“HTTP_REFERER”]: // 获取来源url  
  46.       
  47.     // 如果来源和跳转后的地址都不是本站,那么就要给出提示  
  48.     if(!isMyDomain($fromUrl$myDomain)) {  
  49.         $echoTips = true;  
  50.     }  
  51. else {    // 网址参数不完整  
  52.     $url = ‘/’;  
  53.     $title = ‘参数错误,正在返回首页…’;  
  54. }  
  55.   
  56.   
  57. /** 
  58.  * 判断是不是自己的域名 
  59.  * @param $domain 要进行判断的域名 
  60.  * @param $my 自己的域名 
  61.  * @return 对比结果 
  62.  */  
  63. function isMyDomain($domain$my) {  
  64.     preg_match(‘/([^\?]*)/i’, $domain$match);  
  65.     if(isset($match[1])) $domain = $match[1];  
  66.     preg_match(‘/([\w-]*\.[\w-]*)\/.*/i’, $domain.’/’, $match);  
  67.     if(isset($match[1]) && $match[1] == $myreturn true;  
  68.     return false;  
  69. }  
  70.   
  71. ?>  
  72. <html>  
  73. <head>  
  74. <meta http-equiv=“Content-Type” content=“text/html; charset=UTF-8”>  
  75. <meta http-equiv=“X-UA-Compatible” content=“IE=edge”>  
  76. <meta name=“viewport” content=“width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no”>  
  77. <?php  
  78. if($echoTips) {  
  79.     echo ‘<title>跳转提示</title>’;  
  80. else {  
  81.     echo ‘<meta http-equiv=“refresh” content=“0;url=’.$url.'”>’;  
  82.     echo ‘<title>’.$title.'</title>’;  
  83. }  
  84. ?>  
  85. <style>  
  86. body{background:#fff;font-family:Microsoft Yahei;-webkit-animation:fadeIn 1s linear;animation:fadeIn 1s linear}  
  87. @-webkit-keyframes fadeIn{from{opacity:0}  
  88. to{opacity:1}  
  89. }@keyframes fadeIn{from{opacity:0}  
  90. to{opacity:1}  
  91. }#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}  
  92. #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}  
  93. @-webkit-keyframes spinPulse{0%{-webkit-transform:rotate(160deg);opacity:0;box-shadow:0 0 1px #505050}  
  94. 50%{-webkit-transform:rotate(145deg);opacity:1}  
  95. 100%{-webkit-transform:rotate(-320deg);opacity:0}  
  96. }@-webkit-keyframes spinoffPulse{0%{-webkit-transform:rotate(0deg)}  
  97. 100%{-webkit-transform:rotate(360deg)}  
  98. }#loading-text{position:fixed;left:110px;bottom:35px;color:#736D6D}  
  99. @media screen and (max-width:600px){#circle,#circle1{left:0;right:0;top:0;bottom:0}  
  100. #circle{margin:120px auto}  
  101. #circle1{margin:130px auto}  
  102. #loading-text{display:block;text-align:center;margin-top:220px;position:static;margin-left:10px}  
  103. }  
  104. .warning{max-width: 500px;margin: 20px auto;}  
  105. .wtitle {font-size: 22px;color: #d68300;}  
  106. .wurl {overflow: hidden;text-overflow: ellipsis;white-space: nowrap;color: #827777;}  
  107. .btn {display: inline-block;line-height: 20px;cursor: pointer;border: 1px solid #A9A6A6;padding: 6px 10px;font-size: 14px;text-decoration: none;}  
  108. .btn-green {color: #fff;background-color: #238aca;border: 1px solid #238aca;}  
  109. .btn:hover {background-color: #A9A6A6;border: 1px solid #A9A6A6;color: #fff;}  
  110. </style>  
  111. </head>  
  112. <body>  
  113.     <?php if($echoTips) { ?>  
  114.     <div class=“warning”>  
  115.         <p class=“wtitle”>您将要访问:</p>  
  116.         <p class=“wurl” title=“<?php echo $url;?>”><?php echo $url;?></p>  
  117.         <p>该网站不属于孟坤博客,我们无法确认该网页是否安全,它可能包含未知的安全隐患。</p>  
  118.         <a class=“btn btn-green” href=“<?php echo $url;?>” rel=“nofollow”>继续访问</a>  
  119.         <span class=“btn” onclick=“closePage()”>关闭网页</span>  
  120.     </div>  
  121.     <script>  
  122.     function closePage() {  
  123.         // 通用窗口关闭  
  124.         window.opener=null;  
  125.         window.open(,’_self’);  
  126.         window.close();  
  127.         // 微信浏览器关闭  
  128.         WeixinJSBridge.call(‘closeWindow’);  
  129.     }  
  130.     </script>  
  131.     <?php } else { ?>  
  132.     <div id=“circle”></div>  
  133.     <div id=“circle1”></div>  
  134.     <p id=“loading-text”>页面加载中,请稍候…</p>  
  135.     <?php } ?>  
  136. </body>  
  137. </html>  

来源:https://mkblog.cn/701/

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?