前言
在日常开发中,无论是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?:)?\/\/([^/?&]+\.)?(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链接