Skip to content

Commit 9550ea5

Browse files
committed
fix: Prevent SSRF via SMTP host configuration
1 parent 4b68491 commit 9550ea5

7 files changed

Lines changed: 876 additions & 783 deletions

File tree

backend/crm/src/main/java/cn/cordys/crm/system/dto/response/EmailDTO.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
public class EmailDTO {
88
//SMTP 主机
99
@Schema(description = "SMTP 主机")
10+
@SafeHost(message = "{ip_address.not_allowed}")
1011
private String host;
1112
//SMTP 端口
1213
@Schema(description = "SMTP 端口")
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package cn.cordys.crm.system.dto.response;
2+
3+
import jakarta.validation.Constraint;
4+
import jakarta.validation.Payload;
5+
6+
import java.lang.annotation.*;
7+
8+
@Documented
9+
@Constraint(validatedBy = SafeHostValidator.class)
10+
@Target({ElementType.FIELD})
11+
@Retention(RetentionPolicy.RUNTIME)
12+
public @interface SafeHost {
13+
String message() default "不允许使用内网或保留地址";
14+
Class<?>[] groups() default {};
15+
Class<? extends Payload>[] payload() default {};
16+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package cn.cordys.crm.system.dto.response;
2+
3+
import jakarta.validation.ConstraintValidator;
4+
import jakarta.validation.ConstraintValidatorContext;
5+
6+
import java.net.InetAddress;
7+
import java.net.UnknownHostException;
8+
import java.util.Arrays;
9+
import java.util.List;
10+
11+
public class SafeHostValidator implements ConstraintValidator<SafeHost, String> {
12+
13+
private static final List<String> BLOCKED_CIDRS = Arrays.asList(
14+
"10.0.0.0/8",
15+
"172.16.0.0/12",
16+
"192.168.0.0/16",
17+
"127.0.0.0/8",
18+
"169.254.0.0/16",
19+
"0.0.0.0/8",
20+
"100.64.0.0/10",
21+
"224.0.0.0/4",
22+
"::1/128",
23+
"fe80::/10",
24+
"fc00::/7"
25+
);
26+
27+
@Override
28+
public boolean isValid(String host, ConstraintValidatorContext context) {
29+
if (host == null || host.isEmpty()) {
30+
return true; // 由 @NotNull 控制
31+
}
32+
try {
33+
InetAddress addr = InetAddress.getByName(host);
34+
return !isBlocked(addr);
35+
} catch (UnknownHostException e) {
36+
// 无法解析的域名一律放行?建议拒绝,因为可能被用来绕过(可改为 return false)
37+
return false;
38+
}
39+
}
40+
41+
private boolean isBlocked(InetAddress addr) {
42+
for (String cidr : BLOCKED_CIDRS) {
43+
if (matchesCIDR(addr, cidr)) {
44+
return true;
45+
}
46+
}
47+
return false;
48+
}
49+
50+
private boolean matchesCIDR(InetAddress addr, String cidr) {
51+
try {
52+
String[] parts = cidr.split("/");
53+
InetAddress network = InetAddress.getByName(parts[0]);
54+
int prefix = Integer.parseInt(parts[1]);
55+
byte[] addrBytes = addr.getAddress();
56+
byte[] networkBytes = network.getAddress();
57+
if (addrBytes.length != networkBytes.length) return false;
58+
59+
int fullBytes = prefix / 8;
60+
int remainingBits = prefix % 8;
61+
62+
for (int i = 0; i < fullBytes; i++) {
63+
if (addrBytes[i] != networkBytes[i]) return false;
64+
}
65+
if (remainingBits > 0 && fullBytes < addrBytes.length) {
66+
int mask = (0xFF << (8 - remainingBits)) & 0xFF;
67+
return (addrBytes[fullBytes] & mask) == (networkBytes[fullBytes] & mask);
68+
}
69+
return true;
70+
} catch (Exception e) {
71+
return false;
72+
}
73+
}
74+
}

backend/crm/src/main/java/cn/cordys/crm/system/job/NotifyOnJob.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public void onEvent() {
3232
try {
3333
this.addNotification();
3434
} catch (Exception e) {
35-
log.error("公告通知异常: ", e.getMessage());
35+
log.error("公告通知异常: {}", e.getMessage());
3636
}
3737
}
3838

backend/crm/src/main/resources/i18n/cordys-crm_en_US.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,3 +828,4 @@ order.stage.pending_acceptance=Pending acceptance
828828
order.stage.completed=Completed
829829
order.stage.voided=Voided
830830
order.number.length.exceed=Please reconfigure the generation rule if the line number is longer than 50 characters!
831+
ip_address.not_allowed=The IP address cannot be an intranet or reserved address!

0 commit comments

Comments
 (0)