域名白名单校验:那些让人抓狂的坑、真实绕过与实用修复

63 阅读3分钟

前言

在日常开发中,无论是web应用还是android的webview中,都有可能遇到需要对域名进行白名单校验的场景,包括但不限于:

  • 重定向(Web):登录后 next 参数跳转,GET /login?next=https://app.example.com/welcome
  • OAuth / 回调 URL(Web / 第三方登录): 第三方 OAuth 回调 redirect_uri, redirect_uri=https://client.example.com/oauth/callback
  • DeepLink(Mobile / Android):外部链接触发 app 内页面,myapp://example.com/open?target=https://example.com/doc/1
  • WebView 外链跳转(Android WebView):shouldOverrideUrlLoading 中直接 loadUrl(url)
  • SSRF(服务端发起 URL 请求):用户传入 image_url,服务端下载渲染缩略图。 POST /thumbnail { "url":"http://192.168.0.2/admin" }

常见的坑与绕过方式

1. 使用java.net.URL或android.net.URI(android版本<8.1时受影响)获取host

  • 错误实现代码
//extra_url代表被外部控制的url
java.net.URL url = new java.net.URL(extra_url);  
host = url1.getHost();
  • 绕过方式

    绕过方法1:https://www.evil.com\\@www.whitedomain.com/poc.htm,java.net.URL提取的host是www.whitedomain.com

    绕过方法2:http://www.evil.com\\.whitedomain.com,经过java.net.URL的getHost方法提取到的是www.evil.com\\.whitedomain.com,特别是结合indexof/contains/endwith进行白名单校验时,就会被绕过,实际上访问的却是www.evil.com

2.使用java.net.URI获取host但不进行协议类型校验

String[] whiteList=new String[]{"huawei.com","hicloud.com"};  
java.net.URI url=new java.net.URI(inputUrl);  
String inputDomain=url.getHost(); //提取host  
for (String whiteDomain:whiteList)  
{  
if (inputDomain.endsWith("."+whiteDomain)) //www.huawei.com app.hicloud.com  
return true;  
}  
return false;  
}
  • 绕过方式 javascript://www.whitedomain.com/%0d%0awindow.location.href=‘http://www.evil.net/poc.htm‘ 相当于执行了一行js代码,第一行通过//符号来骗过java.net.URI获取到值为www.whitedomain.com的host,恰好//符号在Javascript的世界里是行注释符号,所以第一行实际并没有执行;然后通过%0d%0a换行,继续执行window.location.href=’www.evil.net/poc.htm’

3.自定义正则提取host

  • 错误实现代码-1

trustedurl: [/^(https?:)?\/\/([^/?&amp;]+\.)?(mydomain1|mydomain2|mydomain3)\.(com|cn)\/.*$/i] 它只校验了白名单域名关键字出现在url中,但是没有要求仅能出现在主域名部分

  • 绕过方式

https://evil.com:9998#.mydomain1.com/

  • 错误实现代码2

把://和/中间的字符串提取出来认为是host
String inputDomain=inputUrl.substring(tempStr.indexOf(“://“),tempStr.indexOf("/")); 然后再判断提取出来的域名中有没有包含白名单域名
if(inputDomain.indexOf(whiteDomain)>0) return true

  • 绕过方法

http://whitedomain.com@www.evil.com/poc.htm 这样取出来的域名就是whitedomain.com@www.evil.com,这个域名肯定包含白名单域名,所以就绕过了

4.判断取出的host是否为白名单域名存在问题

-使用contains方法

本意是检查白名单域名字符串有没有包含该url中, 错误实现方式:

{  
String[] whiteList=new String[]{"whitedomain1.com","whitedomain2.com"};  
for (String whiteDomain:whiteList)  
{  
if (inputUrl.contains(whiteDomain)>0)  
return true;  
}  
return false;  
}

绕过方式:任何可以添加字符串的字段

  • 子域名whitedomain.com.evil.com

  • 子路径 evil.com/whitedomain.com

  • 参数 evil.com/xxxx#whitedomain.com、www.evil.com/poc.html?whitedomain.com

-使用indexOf方法

本意是检查白名单域名字符串有没有出现在url中,错误实现方式:

{  
String[] whiteList=new String[]{"whitedomain1.com","whitedomain2.com"};  
for (String whiteDomain:whiteList)  
{  
if (inputUrl.indexOf(whiteDomain)>0)  
return true;  
}  
return false;  
}

绕过方法:与cntains绕过方法一样

-使用startWith、endsWith方法

检查从url提取的域名字符串是不是以白名单域名开头或者结尾,一般白名单会有子域名,因此不用equal 错误写法:

String[] whiteList = new String[]{"whitedomain1.com", "whitedomain2.com"};  
  
for (String whiteDomain : whiteList) {  
// ❌ 这里的 startsWith 和 endsWith 存在绕过风险  
if (inputUrl.startsWith(whiteDomain) || inputUrl.endsWith(whiteDomain)) {  
return true;  
}  
}  
return false;  
}

绕过方式: startsWith whitedomain.com.evil.com endsWith evilwhitedomaini.com

建议写法

* 校验 URL 是否符合白名单域名  
*  
* @param url 待校验的 URL  
* @return 是否在白名单内  
*/  
private boolean checkDomain(String url) {  
//  
if (!(url.startsWith("https://") || url.startsWith("http://"))) {
    return false;
}
  
// 2. 定义白名单域名列表注意域名前面要加.号  
String[] whiteList = new String[]{".whitedomain1.com", ".whitedomain2.com"};  
//由 于 android.net.Uri 以 及java.net.URL 存在漏洞,推荐使用 java.net.URI 对 url 字符串解析。 
java.net.URI javaUri;  
try {  
// 3. 解析 URL,推荐使用 java.net.URI  
javaUri = new java.net.URI(url);  
} catch (java.net.URISyntaxException e) {  
return false;  
}  
  
// 4. 提取域名  
String inputDomain = javaUri.getHost();  
if (inputDomain == null) {  
return false;  
}  
  
Log.d(Secret.TAG, "inputDomain: " + inputDomain);  
  
// 5. 进行白名单校验  
for (String whiteDomain : whiteList) {  
// 确保白名单域名前带有 "."  
String domainToCheck = whiteDomain.startsWith(".") ? whiteDomain : "." + whiteDomain;  
  
// 6. 确保 `inputDomain` 完整匹配 `whiteDomain`,防止子域欺骗  
if (inputDomain.equals(domainToCheck.substring(1)) ||  
(inputDomain.endsWith(domainToCheck) && inputDomain.charAt(inputDomain.length() - domainToCheck.length() - 1) == '.')) {  
return true;  
}  
}  
  
return false;  
}

附录

一些用于测试的恶意url链接

恶意 URL浏览器解析的 host(常见解析结果)
www.evil.com/path/to/res…www.evil.com
id.whitedomain.com@evil.com/evil.com
id.whitedomain.com%25%2F@evil.com.//##evil.com
id.whitedomain.com%40@evil.com/evil.com
https%3A%2F%2Fwww.whitedomain.com%252F@evil.com%2F%0Aevil.com
evil.com#.whitedomain.com/evil.com
www.evil.com/poc.html?wh…www.evil.com
id.whitedomain.com@www.evil.com/poc.htmwww.evil.com
id.whitedomain.com.evil.com/poc.html id.whitedomain.com.evil.com
www.evil.com\\\\@www.whitedomain.com/poc.htmwww.evil.com(可以在调试窗口的network中看到这条请求的authority是www.evil.com)
www.evil.com\\\\.whitedomain.comwww.evil.com (可以在调试窗口的network中看到这条请求的authority是www.evil.com)
JavaScript://www.whitedomain.com/%0d%0awindo…www.evil.com/poc.htmwww.whitedomain.com(scheme 为 javascript)
www.evil.com//@id.whited…www.evil.com(//@... 多数情况会被当作 path 部分)
www.evil.com//.whitedoma…www.evil.com
evilwhitedomain.comevilwhitedomain.com,可能是攻击者控制的域名
https%3A%2F%2Fwww.evil.com%5c@www.whitedomain.com/www.evil.com
https%3A%2F%2Fwww.whitedomain.com%25252%2525123456@www.evil.com (经两次 URL 解码可到 www.whitedomain.com%2%123456@www.evil.com)www.evil.com