diff --git a/common/ip.go b/common/ip.go
new file mode 100644
index 000000000..bfb64ee7f
--- /dev/null
+++ b/common/ip.go
@@ -0,0 +1,22 @@
+package common
+
+import "net"
+
+func IsPrivateIP(ip net.IP) bool {
+ if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
+ return true
+ }
+
+ private := []net.IPNet{
+ {IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
+ {IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
+ {IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
+ }
+
+ for _, privateNet := range private {
+ if privateNet.Contains(ip) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/common/ssrf_protection.go b/common/ssrf_protection.go
new file mode 100644
index 000000000..b0988d907
--- /dev/null
+++ b/common/ssrf_protection.go
@@ -0,0 +1,384 @@
+package common
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+// SSRFProtection SSRF防护配置
+type SSRFProtection struct {
+ AllowPrivateIp bool
+ WhitelistDomains []string // domain format, e.g. example.com, *.example.com
+ WhitelistIps []string // CIDR format
+ AllowedPorts []int // 允许的端口范围
+}
+
+// DefaultSSRFProtection 默认SSRF防护配置
+var DefaultSSRFProtection = &SSRFProtection{
+ AllowPrivateIp: false,
+ WhitelistDomains: []string{},
+ WhitelistIps: []string{},
+ AllowedPorts: []int{},
+}
+
+// isPrivateIP 检查IP是否为私有地址
+func isPrivateIP(ip net.IP) bool {
+ if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
+ return true
+ }
+
+ // 检查私有网段
+ private := []net.IPNet{
+ {IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 10.0.0.0/8
+ {IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)}, // 172.16.0.0/12
+ {IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)}, // 192.168.0.0/16
+ {IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 127.0.0.0/8
+ {IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)}, // 169.254.0.0/16 (链路本地)
+ {IP: net.IPv4(224, 0, 0, 0), Mask: net.CIDRMask(4, 32)}, // 224.0.0.0/4 (组播)
+ {IP: net.IPv4(240, 0, 0, 0), Mask: net.CIDRMask(4, 32)}, // 240.0.0.0/4 (保留)
+ }
+
+ for _, privateNet := range private {
+ if privateNet.Contains(ip) {
+ return true
+ }
+ }
+
+ // 检查IPv6私有地址
+ if ip.To4() == nil {
+ // IPv6 loopback
+ if ip.Equal(net.IPv6loopback) {
+ return true
+ }
+ // IPv6 link-local
+ if strings.HasPrefix(ip.String(), "fe80:") {
+ return true
+ }
+ // IPv6 unique local
+ if strings.HasPrefix(ip.String(), "fc") || strings.HasPrefix(ip.String(), "fd") {
+ return true
+ }
+ }
+
+ return false
+}
+
+// parsePortRanges 解析端口范围配置
+// 支持格式: "80", "443", "8000-9000"
+func parsePortRanges(portConfigs []string) ([]int, error) {
+ var ports []int
+
+ for _, config := range portConfigs {
+ config = strings.TrimSpace(config)
+ if config == "" {
+ continue
+ }
+
+ if strings.Contains(config, "-") {
+ // 处理端口范围 "8000-9000"
+ parts := strings.Split(config, "-")
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("invalid port range format: %s", config)
+ }
+
+ startPort, err := strconv.Atoi(strings.TrimSpace(parts[0]))
+ if err != nil {
+ return nil, fmt.Errorf("invalid start port in range %s: %v", config, err)
+ }
+
+ endPort, err := strconv.Atoi(strings.TrimSpace(parts[1]))
+ if err != nil {
+ return nil, fmt.Errorf("invalid end port in range %s: %v", config, err)
+ }
+
+ if startPort > endPort {
+ return nil, fmt.Errorf("invalid port range %s: start port cannot be greater than end port", config)
+ }
+
+ if startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535 {
+ return nil, fmt.Errorf("port range %s contains invalid port numbers (must be 1-65535)", config)
+ }
+
+ // 添加范围内的所有端口
+ for port := startPort; port <= endPort; port++ {
+ ports = append(ports, port)
+ }
+ } else {
+ // 处理单个端口 "80"
+ port, err := strconv.Atoi(config)
+ if err != nil {
+ return nil, fmt.Errorf("invalid port number: %s", config)
+ }
+
+ if port < 1 || port > 65535 {
+ return nil, fmt.Errorf("invalid port number %d (must be 1-65535)", port)
+ }
+
+ ports = append(ports, port)
+ }
+ }
+
+ return ports, nil
+}
+
+// isAllowedPort 检查端口是否被允许
+func (p *SSRFProtection) isAllowedPort(port int) bool {
+ if len(p.AllowedPorts) == 0 {
+ return true // 如果没有配置端口限制,则允许所有端口
+ }
+
+ for _, allowedPort := range p.AllowedPorts {
+ if port == allowedPort {
+ return true
+ }
+ }
+ return false
+}
+
+// isAllowedPortFromRanges 从端口范围字符串检查端口是否被允许
+func isAllowedPortFromRanges(port int, portRanges []string) bool {
+ if len(portRanges) == 0 {
+ return true // 如果没有配置端口限制,则允许所有端口
+ }
+
+ allowedPorts, err := parsePortRanges(portRanges)
+ if err != nil {
+ // 如果解析失败,为安全起见拒绝访问
+ return false
+ }
+
+ for _, allowedPort := range allowedPorts {
+ if port == allowedPort {
+ return true
+ }
+ }
+ return false
+}
+
+// isDomainWhitelisted 检查域名是否在白名单中
+func (p *SSRFProtection) isDomainWhitelisted(domain string) bool {
+ if len(p.WhitelistDomains) == 0 {
+ return false
+ }
+
+ domain = strings.ToLower(domain)
+ for _, whitelistDomain := range p.WhitelistDomains {
+ whitelistDomain = strings.ToLower(whitelistDomain)
+
+ // 精确匹配
+ if domain == whitelistDomain {
+ return true
+ }
+
+ // 通配符匹配 (*.example.com)
+ if strings.HasPrefix(whitelistDomain, "*.") {
+ suffix := strings.TrimPrefix(whitelistDomain, "*.")
+ if strings.HasSuffix(domain, "."+suffix) || domain == suffix {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// isIPWhitelisted 检查IP是否在白名单中
+func (p *SSRFProtection) isIPWhitelisted(ip net.IP) bool {
+ if len(p.WhitelistIps) == 0 {
+ return false
+ }
+
+ for _, whitelistCIDR := range p.WhitelistIps {
+ _, network, err := net.ParseCIDR(whitelistCIDR)
+ if err != nil {
+ // 尝试作为单个IP处理
+ if whitelistIP := net.ParseIP(whitelistCIDR); whitelistIP != nil {
+ if ip.Equal(whitelistIP) {
+ return true
+ }
+ }
+ continue
+ }
+
+ if network.Contains(ip) {
+ return true
+ }
+ }
+ return false
+}
+
+// IsIPAccessAllowed 检查IP是否允许访问
+func (p *SSRFProtection) IsIPAccessAllowed(ip net.IP) bool {
+ // 如果IP在白名单中,直接允许访问(绕过私有IP检查)
+ if p.isIPWhitelisted(ip) {
+ return true
+ }
+
+ // 如果IP白名单为空,允许所有IP(但仍需通过私有IP检查)
+ if len(p.WhitelistIps) == 0 {
+ // 检查私有IP限制
+ if isPrivateIP(ip) && !p.AllowPrivateIp {
+ return false
+ }
+ return true
+ }
+
+ // 如果IP白名单不为空且IP不在白名单中,拒绝访问
+ return false
+}
+
+// ValidateURL 验证URL是否安全
+func (p *SSRFProtection) ValidateURL(urlStr string) error {
+ // 解析URL
+ u, err := url.Parse(urlStr)
+ if err != nil {
+ return fmt.Errorf("invalid URL format: %v", err)
+ }
+
+ // 只允许HTTP/HTTPS协议
+ if u.Scheme != "http" && u.Scheme != "https" {
+ return fmt.Errorf("unsupported protocol: %s (only http/https allowed)", u.Scheme)
+ }
+
+ // 解析主机和端口
+ host, portStr, err := net.SplitHostPort(u.Host)
+ if err != nil {
+ // 没有端口,使用默认端口
+ host = u.Host
+ if u.Scheme == "https" {
+ portStr = "443"
+ } else {
+ portStr = "80"
+ }
+ }
+
+ // 验证端口
+ port, err := strconv.Atoi(portStr)
+ if err != nil {
+ return fmt.Errorf("invalid port: %s", portStr)
+ }
+
+ if !p.isAllowedPort(port) {
+ return fmt.Errorf("port %d is not allowed", port)
+ }
+
+ // 检查域名白名单
+ if p.isDomainWhitelisted(host) {
+ return nil // 白名单域名直接通过
+ }
+
+ // DNS解析获取IP地址
+ ips, err := net.LookupIP(host)
+ if err != nil {
+ return fmt.Errorf("DNS resolution failed for %s: %v", host, err)
+ }
+
+ // 检查所有解析的IP地址
+ for _, ip := range ips {
+ if !p.IsIPAccessAllowed(ip) {
+ if isPrivateIP(ip) {
+ return fmt.Errorf("private IP address not allowed: %s resolves to %s", host, ip.String())
+ } else {
+ return fmt.Errorf("IP address not in whitelist: %s resolves to %s", host, ip.String())
+ }
+ }
+ }
+
+ return nil
+}
+
+// ValidateURLWithDefaults 使用默认配置验证URL
+func ValidateURLWithDefaults(urlStr string) error {
+ return DefaultSSRFProtection.ValidateURL(urlStr)
+}
+
+// ValidateURLWithFetchSetting 使用FetchSetting配置验证URL
+func ValidateURLWithFetchSetting(urlStr string, enableSSRFProtection, allowPrivateIp bool, whitelistDomains, whitelistIps, allowedPorts []string) error {
+ // 如果SSRF防护被禁用,直接返回成功
+ if !enableSSRFProtection {
+ return nil
+ }
+
+ // 解析端口范围配置
+ allowedPortInts, err := parsePortRanges(allowedPorts)
+ if err != nil {
+ return fmt.Errorf("request reject - invalid port configuration: %v", err)
+ }
+
+ protection := &SSRFProtection{
+ AllowPrivateIp: allowPrivateIp,
+ WhitelistDomains: whitelistDomains,
+ WhitelistIps: whitelistIps,
+ AllowedPorts: allowedPortInts,
+ }
+ return protection.ValidateURL(urlStr)
+}
+
+// ValidateURLWithPortRanges 直接使用端口范围字符串验证URL(更高效的版本)
+func ValidateURLWithPortRanges(urlStr string, allowPrivateIp bool, whitelistDomains, whitelistIps, allowedPorts []string) error {
+ // 解析URL
+ u, err := url.Parse(urlStr)
+ if err != nil {
+ return fmt.Errorf("invalid URL format: %v", err)
+ }
+
+ // 只允许HTTP/HTTPS协议
+ if u.Scheme != "http" && u.Scheme != "https" {
+ return fmt.Errorf("unsupported protocol: %s (only http/https allowed)", u.Scheme)
+ }
+
+ // 解析主机和端口
+ host, portStr, err := net.SplitHostPort(u.Host)
+ if err != nil {
+ // 没有端口,使用默认端口
+ host = u.Host
+ if u.Scheme == "https" {
+ portStr = "443"
+ } else {
+ portStr = "80"
+ }
+ }
+
+ // 验证端口
+ port, err := strconv.Atoi(portStr)
+ if err != nil {
+ return fmt.Errorf("invalid port: %s", portStr)
+ }
+
+ if !isAllowedPortFromRanges(port, allowedPorts) {
+ return fmt.Errorf("port %d is not allowed", port)
+ }
+
+ // 创建临时的SSRFProtection来复用域名和IP检查逻辑
+ protection := &SSRFProtection{
+ AllowPrivateIp: allowPrivateIp,
+ WhitelistDomains: whitelistDomains,
+ WhitelistIps: whitelistIps,
+ }
+
+ // 检查域名白名单
+ if protection.isDomainWhitelisted(host) {
+ return nil // 白名单域名直接通过
+ }
+
+ // DNS解析获取IP地址
+ ips, err := net.LookupIP(host)
+ if err != nil {
+ return fmt.Errorf("DNS resolution failed for %s: %v", host, err)
+ }
+
+ // 检查所有解析的IP地址
+ for _, ip := range ips {
+ if !protection.IsIPAccessAllowed(ip) {
+ if isPrivateIP(ip) {
+ return fmt.Errorf("private IP address not allowed: %s resolves to %s", host, ip.String())
+ } else {
+ return fmt.Errorf("IP address not in whitelist: %s resolves to %s", host, ip.String())
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/service/cf_worker.go b/service/download.go
similarity index 52%
rename from service/cf_worker.go
rename to service/download.go
index 4a7b43760..2f30870d4 100644
--- a/service/cf_worker.go
+++ b/service/download.go
@@ -6,7 +6,7 @@ import (
"fmt"
"net/http"
"one-api/common"
- "one-api/setting"
+ "one-api/setting/system_setting"
"strings"
)
@@ -21,14 +21,20 @@ type WorkerRequest struct {
// DoWorkerRequest 通过Worker发送请求
func DoWorkerRequest(req *WorkerRequest) (*http.Response, error) {
- if !setting.EnableWorker() {
+ if !system_setting.EnableWorker() {
return nil, fmt.Errorf("worker not enabled")
}
- if !setting.WorkerAllowHttpImageRequestEnabled && !strings.HasPrefix(req.URL, "https") {
+ if !system_setting.WorkerAllowHttpImageRequestEnabled && !strings.HasPrefix(req.URL, "https") {
return nil, fmt.Errorf("only support https url")
}
- workerUrl := setting.WorkerUrl
+ // SSRF防护:验证请求URL
+ fetchSetting := system_setting.GetFetchSetting()
+ if err := common.ValidateURLWithFetchSetting(req.URL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.WhitelistDomains, fetchSetting.WhitelistIps, fetchSetting.AllowedPorts); err != nil {
+ return nil, fmt.Errorf("request reject: %v", err)
+ }
+
+ workerUrl := system_setting.WorkerUrl
if !strings.HasSuffix(workerUrl, "/") {
workerUrl += "/"
}
@@ -43,15 +49,21 @@ func DoWorkerRequest(req *WorkerRequest) (*http.Response, error) {
}
func DoDownloadRequest(originUrl string, reason ...string) (resp *http.Response, err error) {
- if setting.EnableWorker() {
+ if system_setting.EnableWorker() {
common.SysLog(fmt.Sprintf("downloading file from worker: %s, reason: %s", originUrl, strings.Join(reason, ", ")))
req := &WorkerRequest{
URL: originUrl,
- Key: setting.WorkerValidKey,
+ Key: system_setting.WorkerValidKey,
}
return DoWorkerRequest(req)
} else {
- common.SysLog(fmt.Sprintf("downloading from origin with worker: %s, reason: %s", originUrl, strings.Join(reason, ", ")))
+ // SSRF防护:验证请求URL(非Worker模式)
+ fetchSetting := system_setting.GetFetchSetting()
+ if err := common.ValidateURLWithFetchSetting(originUrl, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.WhitelistDomains, fetchSetting.WhitelistIps, fetchSetting.AllowedPorts); err != nil {
+ return nil, fmt.Errorf("request reject: %v", err)
+ }
+
+ common.SysLog(fmt.Sprintf("downloading from origin: %s, reason: %s", common.MaskSensitiveInfo(originUrl), strings.Join(reason, ", ")))
return http.Get(originUrl)
}
}
diff --git a/service/user_notify.go b/service/user_notify.go
index c4a3ea91f..f9d7b6691 100644
--- a/service/user_notify.go
+++ b/service/user_notify.go
@@ -7,7 +7,7 @@ import (
"one-api/common"
"one-api/dto"
"one-api/model"
- "one-api/setting"
+ "one-api/setting/system_setting"
"strings"
)
@@ -91,11 +91,11 @@ func sendBarkNotify(barkURL string, data dto.Notify) error {
var resp *http.Response
var err error
- if setting.EnableWorker() {
+ if system_setting.EnableWorker() {
// 使用worker发送请求
workerReq := &WorkerRequest{
URL: finalURL,
- Key: setting.WorkerValidKey,
+ Key: system_setting.WorkerValidKey,
Method: http.MethodGet,
Headers: map[string]string{
"User-Agent": "OneAPI-Bark-Notify/1.0",
@@ -113,6 +113,12 @@ func sendBarkNotify(barkURL string, data dto.Notify) error {
return fmt.Errorf("bark request failed with status code: %d", resp.StatusCode)
}
} else {
+ // SSRF防护:验证Bark URL(非Worker模式)
+ fetchSetting := system_setting.GetFetchSetting()
+ if err := common.ValidateURLWithFetchSetting(finalURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.WhitelistDomains, fetchSetting.WhitelistIps, fetchSetting.AllowedPorts); err != nil {
+ return fmt.Errorf("request reject: %v", err)
+ }
+
// 直接发送请求
req, err = http.NewRequest(http.MethodGet, finalURL, nil)
if err != nil {
diff --git a/service/webhook.go b/service/webhook.go
index 8faccda30..1f159eb4b 100644
--- a/service/webhook.go
+++ b/service/webhook.go
@@ -8,8 +8,9 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "one-api/common"
"one-api/dto"
- "one-api/setting"
+ "one-api/setting/system_setting"
"time"
)
@@ -56,11 +57,11 @@ func SendWebhookNotify(webhookURL string, secret string, data dto.Notify) error
var req *http.Request
var resp *http.Response
- if setting.EnableWorker() {
+ if system_setting.EnableWorker() {
// 构建worker请求数据
workerReq := &WorkerRequest{
URL: webhookURL,
- Key: setting.WorkerValidKey,
+ Key: system_setting.WorkerValidKey,
Method: http.MethodPost,
Headers: map[string]string{
"Content-Type": "application/json",
@@ -86,6 +87,12 @@ func SendWebhookNotify(webhookURL string, secret string, data dto.Notify) error
return fmt.Errorf("webhook request failed with status code: %d", resp.StatusCode)
}
} else {
+ // SSRF防护:验证Webhook URL(非Worker模式)
+ fetchSetting := system_setting.GetFetchSetting()
+ if err := common.ValidateURLWithFetchSetting(webhookURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.WhitelistDomains, fetchSetting.WhitelistIps, fetchSetting.AllowedPorts); err != nil {
+ return fmt.Errorf("request reject: %v", err)
+ }
+
req, err = http.NewRequest(http.MethodPost, webhookURL, bytes.NewBuffer(payloadBytes))
if err != nil {
return fmt.Errorf("failed to create webhook request: %v", err)
diff --git a/setting/system_setting/fetch_setting.go b/setting/system_setting/fetch_setting.go
new file mode 100644
index 000000000..6e47c3f06
--- /dev/null
+++ b/setting/system_setting/fetch_setting.go
@@ -0,0 +1,28 @@
+package system_setting
+
+import "one-api/setting/config"
+
+type FetchSetting struct {
+ EnableSSRFProtection bool `json:"enable_ssrf_protection"` // 是否启用SSRF防护
+ AllowPrivateIp bool `json:"allow_private_ip"`
+ WhitelistDomains []string `json:"whitelist_domains"` // domain format, e.g. example.com, *.example.com
+ WhitelistIps []string `json:"whitelist_ips"` // CIDR format
+ AllowedPorts []string `json:"allowed_ports"` // port range format, e.g. 80, 443, 8000-9000
+}
+
+var defaultFetchSetting = FetchSetting{
+ EnableSSRFProtection: true, // 默认开启SSRF防护
+ AllowPrivateIp: false,
+ WhitelistDomains: []string{},
+ WhitelistIps: []string{},
+ AllowedPorts: []string{"80", "443", "8080", "8443"},
+}
+
+func init() {
+ // 注册到全局配置管理器
+ config.GlobalConfig.Register("fetch_setting", &defaultFetchSetting)
+}
+
+func GetFetchSetting() *FetchSetting {
+ return &defaultFetchSetting
+}
diff --git a/types/error.go b/types/error.go
index 883ee0641..a42e84385 100644
--- a/types/error.go
+++ b/types/error.go
@@ -122,6 +122,9 @@ func (e *NewAPIError) MaskSensitiveError() string {
return string(e.errorCode)
}
errStr := e.Err.Error()
+ if e.errorCode == ErrorCodeCountTokenFailed {
+ return errStr
+ }
return common.MaskSensitiveInfo(errStr)
}
@@ -153,8 +156,9 @@ func (e *NewAPIError) ToOpenAIError() OpenAIError {
Code: e.errorCode,
}
}
-
- result.Message = common.MaskSensitiveInfo(result.Message)
+ if e.errorCode != ErrorCodeCountTokenFailed {
+ result.Message = common.MaskSensitiveInfo(result.Message)
+ }
return result
}
@@ -178,7 +182,9 @@ func (e *NewAPIError) ToClaudeError() ClaudeError {
Type: string(e.errorType),
}
}
- result.Message = common.MaskSensitiveInfo(result.Message)
+ if e.errorCode != ErrorCodeCountTokenFailed {
+ result.Message = common.MaskSensitiveInfo(result.Message)
+ }
return result
}
diff --git a/web/src/components/settings/SystemSetting.jsx b/web/src/components/settings/SystemSetting.jsx
index 9c7eeaadc..71dfaac8d 100644
--- a/web/src/components/settings/SystemSetting.jsx
+++ b/web/src/components/settings/SystemSetting.jsx
@@ -44,6 +44,7 @@ import { useTranslation } from 'react-i18next';
const SystemSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({
+
PasswordLoginEnabled: '',
PasswordRegisterEnabled: '',
EmailVerificationEnabled: '',
@@ -87,6 +88,12 @@ const SystemSetting = () => {
LinuxDOClientSecret: '',
LinuxDOMinimumTrustLevel: '',
ServerAddress: '',
+ // SSRF防护配置
+ 'fetch_setting.enable_ssrf_protection': true,
+ 'fetch_setting.allow_private_ip': '',
+ 'fetch_setting.whitelist_domains': [],
+ 'fetch_setting.whitelist_ips': [],
+ 'fetch_setting.allowed_ports': [],
});
const [originInputs, setOriginInputs] = useState({});
@@ -98,6 +105,9 @@ const SystemSetting = () => {
useState(false);
const [linuxDOOAuthEnabled, setLinuxDOOAuthEnabled] = useState(false);
const [emailToAdd, setEmailToAdd] = useState('');
+ const [whitelistDomains, setWhitelistDomains] = useState([]);
+ const [whitelistIps, setWhitelistIps] = useState([]);
+ const [allowedPorts, setAllowedPorts] = useState([]);
const getOptions = async () => {
setLoading(true);
@@ -113,6 +123,34 @@ const SystemSetting = () => {
case 'EmailDomainWhitelist':
setEmailDomainWhitelist(item.value ? item.value.split(',') : []);
break;
+ case 'fetch_setting.allow_private_ip':
+ case 'fetch_setting.enable_ssrf_protection':
+ item.value = toBoolean(item.value);
+ break;
+ case 'fetch_setting.whitelist_domains':
+ try {
+ const domains = item.value ? JSON.parse(item.value) : [];
+ setWhitelistDomains(Array.isArray(domains) ? domains : []);
+ } catch (e) {
+ setWhitelistDomains([]);
+ }
+ break;
+ case 'fetch_setting.whitelist_ips':
+ try {
+ const ips = item.value ? JSON.parse(item.value) : [];
+ setWhitelistIps(Array.isArray(ips) ? ips : []);
+ } catch (e) {
+ setWhitelistIps([]);
+ }
+ break;
+ case 'fetch_setting.allowed_ports':
+ try {
+ const ports = item.value ? JSON.parse(item.value) : [];
+ setAllowedPorts(Array.isArray(ports) ? ports : []);
+ } catch (e) {
+ setAllowedPorts(['80', '443', '8080', '8443']);
+ }
+ break;
case 'PasswordLoginEnabled':
case 'PasswordRegisterEnabled':
case 'EmailVerificationEnabled':
@@ -276,6 +314,38 @@ const SystemSetting = () => {
}
};
+ const submitSSRF = async () => {
+ const options = [];
+
+ // 处理域名白名单
+ if (Array.isArray(whitelistDomains)) {
+ options.push({
+ key: 'fetch_setting.whitelist_domains',
+ value: JSON.stringify(whitelistDomains),
+ });
+ }
+
+ // 处理IP白名单
+ if (Array.isArray(whitelistIps)) {
+ options.push({
+ key: 'fetch_setting.whitelist_ips',
+ value: JSON.stringify(whitelistIps),
+ });
+ }
+
+ // 处理端口配置
+ if (Array.isArray(allowedPorts)) {
+ options.push({
+ key: 'fetch_setting.allowed_ports',
+ value: JSON.stringify(allowedPorts),
+ });
+ }
+
+ if (options.length > 0) {
+ await updateOptions(options);
+ }
+ };
+
const handleAddEmail = () => {
if (emailToAdd && emailToAdd.trim() !== '') {
const domain = emailToAdd.trim();
@@ -587,6 +657,136 @@ const SystemSetting = () => {
+
+
+
+ {t('配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全')}
+
+
+
+
+ handleCheckboxChange('fetch_setting.enable_ssrf_protection', e)
+ }
+ >
+ {t('启用SSRF防护(推荐开启以保护服务器安全)')}
+
+
+
+
+
+
+
+ handleCheckboxChange('fetch_setting.allow_private_ip', e)
+ }
+ >
+ {t('允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)')}
+
+
+
+
+
+
+ {t('域名白名单')}
+
+ {t('支持通配符格式,如:example.com, *.api.example.com')}
+
+ {
+ setWhitelistDomains(value);
+ // 触发Form的onChange事件
+ setInputs(prev => ({
+ ...prev,
+ 'fetch_setting.whitelist_domains': value
+ }));
+ }}
+ placeholder={t('输入域名后回车,如:example.com')}
+ style={{ width: '100%' }}
+ />
+
+ {t('域名白名单详细说明')}
+
+
+
+
+
+
+ {t('IP白名单')}
+
+ {t('支持CIDR格式,如:8.8.8.8, 192.168.1.0/24')}
+
+ {
+ setWhitelistIps(value);
+ // 触发Form的onChange事件
+ setInputs(prev => ({
+ ...prev,
+ 'fetch_setting.whitelist_ips': value
+ }));
+ }}
+ placeholder={t('输入IP地址后回车,如:8.8.8.8')}
+ style={{ width: '100%' }}
+ />
+
+ {t('IP白名单详细说明')}
+
+
+
+
+
+
+ {t('允许的端口')}
+
+ {t('支持单个端口和端口范围,如:80, 443, 8000-8999')}
+
+ {
+ setAllowedPorts(value);
+ // 触发Form的onChange事件
+ setInputs(prev => ({
+ ...prev,
+ 'fetch_setting.allowed_ports': value
+ }));
+ }}
+ placeholder={t('输入端口后回车,如:80 或 8000-8999')}
+ style={{ width: '100%' }}
+ />
+
+ {t('端口配置详细说明')}
+
+
+
+
+
+
+
+
|