feat(UrlSecurityUtils): 增强URL安全性检查,支持IPv6过渡地址的嵌入IPv4解析

- 更新了URL安全性检查逻辑,增加对IPv6过渡地址(如NAT64、6to4、Teredo、IPv4兼容地址)的支持。
- 新增方法以提取嵌入的IPv4地址,确保更全面的内网地址检测。
- 在LinkServiceImpl中集成URL验证,确保RSS地址的安全性。
This commit is contained in:
刘宇阳
2026-09-02 09:15:15 +08:00
parent df6ac66648
commit 2d0e9f2c69
3 changed files with 95 additions and 3 deletions

View File

@@ -20,7 +20,7 @@ import java.util.Locale;
* 1) 仅允许 http/https 协议(拒绝 file/gopher/ftp 等协议)。
* 2) host 必须存在,且禁止 localhost。
* 3) 对 host 做 DNS 解析,任一解析结果命中内网/本地地址则整体拒绝。
* 4) 同时覆盖 IPv4 与 IPv6 的回环、私网、链路本地等地址段
* 4) 同时覆盖 IPv4 与 IPv6 的回环、私网、链路本地,以及 IPv6 过渡地址NAT64、6to4、Teredo、IPv4-compatible内嵌的 IPv4
*
* 推荐用法:
* - 写入时校验:阻止恶意 URL 入库(第一道防线)。
@@ -102,14 +102,62 @@ public final class UrlSecurityUtils {
}
if (address instanceof Inet6Address) {
return address.isLoopbackAddress()
if (address.isLoopbackAddress()
|| address.isSiteLocalAddress()
|| address.isLinkLocalAddress()
|| address.isAnyLocalAddress()
|| address.getHostAddress().startsWith("fc")
|| address.getHostAddress().startsWith("fd");
|| address.getHostAddress().startsWith("fd")) {
return true;
}
InetAddress embeddedIpv4 = extractEmbeddedIpv4FromTransition(address.getAddress());
if (embeddedIpv4 != null) {
return isInternalAddress(embeddedIpv4);
}
}
return false;
}
private static InetAddress extractEmbeddedIpv4FromTransition(byte[] bytes) {
if (bytes == null || bytes.length != 16) {
return null;
}
try {
if (bytes[0] == 0x00 && bytes[1] == 0x64 && (bytes[2] & 0xFF) == 0xFF && (bytes[3] & 0xFF) == 0x9B) {
return InetAddress.getByAddress(new byte[]{bytes[12], bytes[13], bytes[14], bytes[15]});
}
if ((bytes[0] & 0xFF) == 0x20 && bytes[1] == 0x02) {
return InetAddress.getByAddress(new byte[]{bytes[2], bytes[3], bytes[4], bytes[5]});
}
if ((bytes[0] & 0xFF) == 0x20 && bytes[1] == 0x01 && bytes[2] == 0x00 && bytes[3] == 0x00) {
byte[] ipv4 = new byte[4];
for (int i = 0; i < 4; i++) {
ipv4[i] = (byte) (~bytes[12 + i] & 0xFF);
}
return InetAddress.getByAddress(ipv4);
}
if (isIpv4CompatibleAddress(bytes)) {
return InetAddress.getByAddress(new byte[]{bytes[12], bytes[13], bytes[14], bytes[15]});
}
} catch (Exception ignored) {
return null;
}
return null;
}
private static boolean isIpv4CompatibleAddress(byte[] bytes) {
for (int i = 0; i < 12; i++) {
if (bytes[i] != 0) {
return false;
}
}
return true;
}
}

View File

@@ -125,6 +125,7 @@ public class LinkServiceImpl extends ServiceImpl<LinkMapper, Link> implements Li
public void editLinkData(LinkFormDTO linkFormDTO) {
Link link = new Link();
BeanUtils.copyProperties(linkFormDTO, link);
UrlSecurityUtils.validateExternalHttpUrl("RSS 地址", link.getRss());
updateById(link);
}

View File

@@ -0,0 +1,43 @@
package liuyuyang.net.core.utils;
import liuyuyang.net.core.execption.CustomException;
import org.junit.Test;
public class UrlSecurityUtilsTest {
@Test(expected = CustomException.class)
public void rejectsNat64Loopback() {
UrlSecurityUtils.validateExternalHttpUrl("RSS 地址", "http://[64:ff9b::7f00:1]/rss.xml");
}
@Test(expected = CustomException.class)
public void rejects6to4PrivateNetwork() {
UrlSecurityUtils.validateExternalHttpUrl("RSS 地址", "http://[2002:c0a8:0101::]/rss.xml");
}
@Test(expected = CustomException.class)
public void rejectsTeredoPrivateNetwork() {
UrlSecurityUtils.validateExternalHttpUrl("RSS 地址", "http://[2001:0000::3f57:ffd2]/rss.xml");
}
@Test(expected = CustomException.class)
public void rejectsIpv4CompatiblePrivateNetwork() {
UrlSecurityUtils.validateExternalHttpUrl("RSS 地址", "http://[::10.0.0.1]/rss.xml");
}
@Test(expected = CustomException.class)
public void rejectsPlainLoopback() {
UrlSecurityUtils.validateExternalHttpUrl("RSS 地址", "http://127.0.0.1/rss.xml");
}
@Test
public void allowsPublicIpv4() {
UrlSecurityUtils.validateExternalHttpUrl("RSS 地址", "http://8.8.8.8/rss.xml");
}
@Test
public void allowsEmptyUrl() {
UrlSecurityUtils.validateExternalHttpUrl("RSS 地址", "");
UrlSecurityUtils.validateExternalHttpUrl("RSS 地址", null);
}
}