fix(安全): 修复依赖漏洞并强化安全扫描
主要改动: - 固定 Go 1.25.5 与 CI 校验并更新扫描流程 - 升级 quic-go、x/crypto、req 等依赖并通过 govulncheck - 强化 JWT 校验、TLS 配置与 xlsx 动态加载 - 新增审计豁免清单与校验脚本
This commit is contained in:
16
.github/audit-exceptions.yml
vendored
Normal file
16
.github/audit-exceptions.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
version: 1
|
||||
exceptions:
|
||||
- package: xlsx
|
||||
advisory: "GHSA-4r6h-8v6p-xvw6"
|
||||
severity: high
|
||||
reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2023-30533)"
|
||||
mitigation: "Load only on export; restrict export permissions and data scope"
|
||||
expires_on: "2026-04-05"
|
||||
owner: "security@your-domain"
|
||||
- package: xlsx
|
||||
advisory: "GHSA-5pgg-2g8v-p4x9"
|
||||
severity: high
|
||||
reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2024-22363)"
|
||||
mitigation: "Load only on export; restrict export permissions and data scope"
|
||||
expires_on: "2026-04-05"
|
||||
owner: "security@your-domain"
|
||||
10
.github/workflows/backend-ci.yml
vendored
10
.github/workflows/backend-ci.yml
vendored
@@ -15,8 +15,11 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: backend/go.mod
|
||||
check-latest: true
|
||||
check-latest: false
|
||||
cache: true
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.25.5'
|
||||
- name: Unit tests
|
||||
working-directory: backend
|
||||
run: make test-unit
|
||||
@@ -31,8 +34,11 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: backend/go.mod
|
||||
check-latest: true
|
||||
check-latest: false
|
||||
cache: true
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.25.5'
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
|
||||
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -104,9 +104,14 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
go-version-file: backend/go.mod
|
||||
check-latest: false
|
||||
cache-dependency-path: backend/go.sum
|
||||
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.25.5'
|
||||
|
||||
# Docker setup for GoReleaser
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
60
.github/workflows/security-scan.yml
vendored
Normal file
60
.github/workflows/security-scan.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backend-security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: backend/go.mod
|
||||
check-latest: false
|
||||
cache-dependency-path: backend/go.sum
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.25.5'
|
||||
- name: Run govulncheck
|
||||
working-directory: backend
|
||||
run: |
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
govulncheck ./...
|
||||
- name: Run gosec
|
||||
working-directory: backend
|
||||
run: |
|
||||
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
gosec -severity high -confidence high ./...
|
||||
|
||||
frontend-security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: frontend/pnpm-lock.yaml
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run pnpm audit
|
||||
working-directory: frontend
|
||||
run: |
|
||||
pnpm audit --prod --audit-level=high --json > audit.json || true
|
||||
- name: Check audit exceptions
|
||||
run: |
|
||||
python tools/check_pnpm_audit_exceptions.py \
|
||||
--audit frontend/audit.json \
|
||||
--exceptions .github/audit-exceptions.yml
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -123,3 +123,6 @@ backend/cmd/server/server
|
||||
deploy/docker-compose.override.yml
|
||||
.gocache/
|
||||
vite.config.js
|
||||
!docs/
|
||||
docs/*
|
||||
!docs/dependency-security.md
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
# =============================================================================
|
||||
|
||||
ARG NODE_IMAGE=node:24-alpine
|
||||
ARG GOLANG_IMAGE=golang:1.25-alpine
|
||||
ARG ALPINE_IMAGE=alpine:3.19
|
||||
ARG GOLANG_IMAGE=golang:1.25.5-alpine
|
||||
ARG ALPINE_IMAGE=alpine:3.20
|
||||
ARG GOPROXY=https://goproxy.cn,direct
|
||||
ARG GOSUMDB=sum.golang.google.cn
|
||||
|
||||
|
||||
63
README.md
63
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||
[](https://vuejs.org/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://redis.io/)
|
||||
@@ -44,13 +44,19 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|------------|
|
||||
| Backend | Go 1.21+, Gin, GORM |
|
||||
| Backend | Go 1.25.5, Gin, GORM |
|
||||
| Frontend | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||
| Database | PostgreSQL 15+ |
|
||||
| Cache/Queue | Redis 7+ |
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- Dependency Security: `docs/dependency-security.md`
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Method 1: Script Installation (Recommended)
|
||||
@@ -160,6 +166,22 @@ ADMIN_PASSWORD=your_admin_password
|
||||
|
||||
# Optional: Custom port
|
||||
SERVER_PORT=8080
|
||||
|
||||
# Optional: Security configuration
|
||||
# Enable URL allowlist validation (false to skip allowlist checks, only basic format validation)
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
|
||||
# Allow insecure HTTP URLs when allowlist is disabled (default: false, requires https)
|
||||
# ⚠️ WARNING: Enabling this allows HTTP (plaintext) URLs which can expose API keys
|
||||
# Only recommended for:
|
||||
# - Development/testing environments
|
||||
# - Internal networks with trusted endpoints
|
||||
# - When using local test servers (http://localhost)
|
||||
# PRODUCTION: Keep this false or use HTTPS URLs only
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=false
|
||||
|
||||
# Allow private IP addresses for upstream/pricing/CRS (for internal deployments)
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=false
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -276,13 +298,48 @@ Additional security-related options are available in `config.yaml`:
|
||||
- `cors.allowed_origins` for CORS allowlist
|
||||
- `security.url_allowlist` for upstream/pricing/CRS host allowlists
|
||||
- `security.url_allowlist.enabled` to disable URL validation (use with caution)
|
||||
- `security.url_allowlist.allow_insecure_http` to allow http URLs when validation is disabled
|
||||
- `security.url_allowlist.allow_insecure_http` to allow HTTP URLs when validation is disabled
|
||||
- `security.url_allowlist.allow_private_hosts` to allow private/local IP addresses
|
||||
- `security.response_headers.enabled` to enable configurable response header filtering (disabled uses default allowlist)
|
||||
- `security.csp` to control Content-Security-Policy headers
|
||||
- `billing.circuit_breaker` to fail closed on billing errors
|
||||
- `server.trusted_proxies` to enable X-Forwarded-For parsing
|
||||
- `turnstile.required` to require Turnstile in release mode
|
||||
|
||||
**⚠️ Security Warning: HTTP URL Configuration**
|
||||
|
||||
When `security.url_allowlist.enabled=false`, the system performs minimal URL validation by default, **rejecting HTTP URLs** and only allowing HTTPS. To allow HTTP URLs (e.g., for development or internal testing), you must explicitly set:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
url_allowlist:
|
||||
enabled: false # Disable allowlist checks
|
||||
allow_insecure_http: true # Allow HTTP URLs (⚠️ INSECURE)
|
||||
```
|
||||
|
||||
**Or via environment variable:**
|
||||
|
||||
```bash
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true
|
||||
```
|
||||
|
||||
**Risks of allowing HTTP:**
|
||||
- API keys and data transmitted in **plaintext** (vulnerable to interception)
|
||||
- Susceptible to **man-in-the-middle (MITM) attacks**
|
||||
- **NOT suitable for production** environments
|
||||
|
||||
**When to use HTTP:**
|
||||
- ✅ Development/testing with local servers (http://localhost)
|
||||
- ✅ Internal networks with trusted endpoints
|
||||
- ✅ Testing account connectivity before obtaining HTTPS
|
||||
- ❌ Production environments (use HTTPS only)
|
||||
|
||||
**Example error without this setting:**
|
||||
```
|
||||
Invalid base URL: invalid url scheme: http
|
||||
```
|
||||
|
||||
If you disable URL validation or response header filtering, harden your network layer:
|
||||
- Enforce an egress allowlist for upstream domains/IPs
|
||||
- Block private/loopback/link-local ranges
|
||||
|
||||
63
README_CN.md
63
README_CN.md
@@ -2,7 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||
[](https://vuejs.org/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://redis.io/)
|
||||
@@ -44,13 +44,19 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅(
|
||||
|
||||
| 组件 | 技术 |
|
||||
|------|------|
|
||||
| 后端 | Go 1.21+, Gin, GORM |
|
||||
| 后端 | Go 1.25.5, Gin, GORM |
|
||||
| 前端 | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||
| 数据库 | PostgreSQL 15+ |
|
||||
| 缓存/队列 | Redis 7+ |
|
||||
|
||||
---
|
||||
|
||||
## 文档
|
||||
|
||||
- 依赖安全:`docs/dependency-security.md`
|
||||
|
||||
---
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 方式一:脚本安装(推荐)
|
||||
@@ -160,6 +166,22 @@ ADMIN_PASSWORD=your_admin_password
|
||||
|
||||
# 可选:自定义端口
|
||||
SERVER_PORT=8080
|
||||
|
||||
# 可选:安全配置
|
||||
# 启用 URL 白名单验证(false 则跳过白名单检查,仅做基本格式校验)
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
|
||||
# 关闭白名单时,是否允许 http:// URL(默认 false,只允许 https://)
|
||||
# ⚠️ 警告:允许 HTTP 会暴露 API 密钥(明文传输)
|
||||
# 仅建议在以下场景使用:
|
||||
# - 开发/测试环境
|
||||
# - 内部可信网络
|
||||
# - 本地测试服务器(http://localhost)
|
||||
# 生产环境:保持 false 或仅使用 HTTPS URL
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=false
|
||||
|
||||
# 是否允许私有 IP 地址用于上游/定价/CRS(内网部署时使用)
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=false
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -276,13 +298,48 @@ default:
|
||||
- `cors.allowed_origins` 配置 CORS 白名单
|
||||
- `security.url_allowlist` 配置上游/价格数据/CRS 主机白名单
|
||||
- `security.url_allowlist.enabled` 可关闭 URL 校验(慎用)
|
||||
- `security.url_allowlist.allow_insecure_http` 关闭校验时允许 http URL
|
||||
- `security.url_allowlist.allow_insecure_http` 关闭校验时允许 HTTP URL
|
||||
- `security.url_allowlist.allow_private_hosts` 允许私有/本地 IP 地址
|
||||
- `security.response_headers.enabled` 可启用可配置响应头过滤(关闭时使用默认白名单)
|
||||
- `security.csp` 配置 Content-Security-Policy
|
||||
- `billing.circuit_breaker` 计费异常时 fail-closed
|
||||
- `server.trusted_proxies` 启用可信代理解析 X-Forwarded-For
|
||||
- `turnstile.required` 在 release 模式强制启用 Turnstile
|
||||
|
||||
**⚠️ 安全警告:HTTP URL 配置**
|
||||
|
||||
当 `security.url_allowlist.enabled=false` 时,系统默认执行最小 URL 校验,**拒绝 HTTP URL**,仅允许 HTTPS。要允许 HTTP URL(例如用于开发或内网测试),必须显式设置:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
url_allowlist:
|
||||
enabled: false # 禁用白名单检查
|
||||
allow_insecure_http: true # 允许 HTTP URL(⚠️ 不安全)
|
||||
```
|
||||
|
||||
**或通过环境变量:**
|
||||
|
||||
```bash
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true
|
||||
```
|
||||
|
||||
**允许 HTTP 的风险:**
|
||||
- API 密钥和数据以**明文传输**(可被截获)
|
||||
- 易受**中间人攻击 (MITM)**
|
||||
- **不适合生产环境**
|
||||
|
||||
**适用场景:**
|
||||
- ✅ 开发/测试环境的本地服务器(http://localhost)
|
||||
- ✅ 内网可信端点
|
||||
- ✅ 获取 HTTPS 前测试账号连通性
|
||||
- ❌ 生产环境(仅使用 HTTPS)
|
||||
|
||||
**未设置此项时的错误示例:**
|
||||
```
|
||||
Invalid base URL: invalid url scheme: http
|
||||
```
|
||||
|
||||
如关闭 URL 校验或响应头过滤,请加强网络层防护:
|
||||
- 出站访问白名单限制上游域名/IP
|
||||
- 阻断私网/回环/链路本地地址
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
module github.com/Wei-Shaw/sub2api
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.11
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/imroc/req/v3 v3.56.0
|
||||
github.com/imroc/req/v3 v3.57.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/spf13/viper v1.18.2
|
||||
@@ -20,16 +18,16 @@ require (
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/zeromicro/go-zero v1.9.4
|
||||
golang.org/x/crypto v0.44.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/term v0.37.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.38.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
@@ -64,7 +62,6 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
@@ -74,10 +71,8 @@ require (
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
||||
github.com/icholy/digest v1.1.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
@@ -105,8 +100,8 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.56.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||
github.com/refraction-networking/utls v1.8.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
@@ -141,16 +136,12 @@ require (
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gorm.io/datatypes v1.2.7 // indirect
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
gorm.io/gorm v1.30.0 // indirect
|
||||
)
|
||||
|
||||
@@ -4,8 +4,6 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
|
||||
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
@@ -96,15 +94,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
|
||||
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -126,8 +121,8 @@ github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZY
|
||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||
github.com/imroc/req/v3 v3.56.0 h1:t6YdqqerYBXhZ9+VjqsQs5wlKxdUNEvsgBhxWc1AEEo=
|
||||
github.com/imroc/req/v3 v3.56.0/go.mod h1:cUZSooE8hhzFNOrAbdxuemXDQxFXLQTnu3066jr7ZGk=
|
||||
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
||||
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
@@ -138,14 +133,10 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
@@ -219,10 +210,10 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
|
||||
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
||||
@@ -335,16 +326,16 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -354,16 +345,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
@@ -386,13 +377,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -147,7 +147,7 @@ type CSPConfig struct {
|
||||
}
|
||||
|
||||
type ProxyProbeConfig struct {
|
||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
|
||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证
|
||||
}
|
||||
|
||||
type BillingConfig struct {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -40,7 +39,7 @@ type Options struct {
|
||||
ProxyURL string // 代理 URL(支持 http/https/socks5/socks5h)
|
||||
Timeout time.Duration // 请求总超时时间
|
||||
ResponseHeaderTimeout time.Duration // 等待响应头超时时间
|
||||
InsecureSkipVerify bool // 是否跳过 TLS 证书验证
|
||||
InsecureSkipVerify bool // 是否跳过 TLS 证书验证(已禁用,不允许设置为 true)
|
||||
ProxyStrict bool // 严格代理模式:代理失败时返回错误而非回退
|
||||
ValidateResolvedIP bool // 是否校验解析后的 IP(防止 DNS Rebinding)
|
||||
AllowPrivateHosts bool // 允许私有地址解析(与 ValidateResolvedIP 一起使用)
|
||||
@@ -113,7 +112,8 @@ func buildTransport(opts Options) (*http.Transport, error) {
|
||||
}
|
||||
|
||||
if opts.InsecureSkipVerify {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
// 安全要求:禁止跳过证书验证,避免中间人攻击。
|
||||
return nil, fmt.Errorf("insecure_skip_verify is not allowed; install a trusted certificate instead")
|
||||
}
|
||||
|
||||
proxyURL := strings.TrimSpace(opts.ProxyURL)
|
||||
|
||||
@@ -24,7 +24,7 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber {
|
||||
validateResolvedIP = cfg.Security.URLAllowlist.Enabled
|
||||
}
|
||||
if insecure {
|
||||
log.Printf("[ProxyProbe] Warning: TLS verification is disabled for proxy probing.")
|
||||
log.Printf("[ProxyProbe] Warning: insecure_skip_verify is not allowed and will cause probe failure.")
|
||||
}
|
||||
return &proxyProbeService{
|
||||
ipInfoURL: defaultIPInfoURL,
|
||||
|
||||
@@ -20,12 +20,16 @@ var (
|
||||
ErrEmailExists = infraerrors.Conflict("EMAIL_EXISTS", "email already exists")
|
||||
ErrInvalidToken = infraerrors.Unauthorized("INVALID_TOKEN", "invalid token")
|
||||
ErrTokenExpired = infraerrors.Unauthorized("TOKEN_EXPIRED", "token has expired")
|
||||
ErrTokenTooLarge = infraerrors.BadRequest("TOKEN_TOO_LARGE", "token too large")
|
||||
ErrTokenRevoked = infraerrors.Unauthorized("TOKEN_REVOKED", "token has been revoked")
|
||||
ErrEmailVerifyRequired = infraerrors.BadRequest("EMAIL_VERIFY_REQUIRED", "email verification is required")
|
||||
ErrRegDisabled = infraerrors.Forbidden("REGISTRATION_DISABLED", "registration is currently disabled")
|
||||
ErrServiceUnavailable = infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "service temporarily unavailable")
|
||||
)
|
||||
|
||||
// maxTokenLength 限制 token 大小,避免超长 header 触发解析时的异常内存分配。
|
||||
const maxTokenLength = 8192
|
||||
|
||||
// JWTClaims JWT载荷数据
|
||||
type JWTClaims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
@@ -309,7 +313,20 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (string
|
||||
|
||||
// ValidateToken 验证JWT token并返回用户声明
|
||||
func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
|
||||
// 先做长度校验,尽早拒绝异常超长 token,降低 DoS 风险。
|
||||
if len(tokenString) > maxTokenLength {
|
||||
return nil, ErrTokenTooLarge
|
||||
}
|
||||
|
||||
// 使用解析器并限制可接受的签名算法,防止算法混淆。
|
||||
parser := jwt.NewParser(jwt.WithValidMethods([]string{
|
||||
jwt.SigningMethodHS256.Name,
|
||||
jwt.SigningMethodHS384.Name,
|
||||
jwt.SigningMethodHS512.Name,
|
||||
}))
|
||||
|
||||
// 保留默认 claims 校验(exp/nbf),避免放行过期或未生效的 token。
|
||||
token, err := parser.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
|
||||
// 验证签名方法
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
|
||||
@@ -140,6 +140,8 @@ func (s *EmailService) SendEmailWithConfig(config *SMTPConfig, to, subject, body
|
||||
func (s *EmailService) sendMailTLS(addr string, auth smtp.Auth, from, to string, msg []byte, host string) error {
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: host,
|
||||
// 强制 TLS 1.2+,避免协议降级导致的弱加密风险。
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
@@ -311,7 +313,11 @@ func (s *EmailService) TestSMTPConnectionWithConfig(config *SMTPConfig) error {
|
||||
addr := fmt.Sprintf("%s:%d", config.Host, config.Port)
|
||||
|
||||
if config.UseTLS {
|
||||
tlsConfig := &tls.Config{ServerName: config.Host}
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: config.Host,
|
||||
// 与发送逻辑一致,显式要求 TLS 1.2+。
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tls connection failed: %w", err)
|
||||
|
||||
390
config.yaml
Normal file
390
config.yaml
Normal file
@@ -0,0 +1,390 @@
|
||||
# Sub2API Configuration File
|
||||
# Sub2API 配置文件
|
||||
#
|
||||
# Copy this file to /etc/sub2api/config.yaml and modify as needed
|
||||
# 复制此文件到 /etc/sub2api/config.yaml 并根据需要修改
|
||||
#
|
||||
# Documentation / 文档: https://github.com/Wei-Shaw/sub2api
|
||||
|
||||
# =============================================================================
|
||||
# Server Configuration
|
||||
# 服务器配置
|
||||
# =============================================================================
|
||||
server:
|
||||
# Bind address (0.0.0.0 for all interfaces)
|
||||
# 绑定地址(0.0.0.0 表示监听所有网络接口)
|
||||
host: "0.0.0.0"
|
||||
# Port to listen on
|
||||
# 监听端口
|
||||
port: 8080
|
||||
# Mode: "debug" for development, "release" for production
|
||||
# 运行模式:"debug" 用于开发,"release" 用于生产环境
|
||||
mode: "release"
|
||||
# Trusted proxies for X-Forwarded-For parsing (CIDR/IP). Empty disables trusted proxies.
|
||||
# 信任的代理地址(CIDR/IP 格式),用于解析 X-Forwarded-For 头。留空则禁用代理信任。
|
||||
trusted_proxies: []
|
||||
|
||||
# =============================================================================
|
||||
# Run Mode Configuration
|
||||
# 运行模式配置
|
||||
# =============================================================================
|
||||
# Run mode: "standard" (default) or "simple" (for internal use)
|
||||
# 运行模式:"standard"(默认)或 "simple"(内部使用)
|
||||
# - standard: Full SaaS features with billing/balance checks
|
||||
# - standard: 完整 SaaS 功能,包含计费和余额校验
|
||||
# - simple: Hides SaaS features and skips billing/balance checks
|
||||
# - simple: 隐藏 SaaS 功能,跳过计费和余额校验
|
||||
run_mode: "standard"
|
||||
|
||||
# =============================================================================
|
||||
# CORS Configuration
|
||||
# 跨域资源共享 (CORS) 配置
|
||||
# =============================================================================
|
||||
cors:
|
||||
# Allowed origins list. Leave empty to disable cross-origin requests.
|
||||
# 允许的来源列表。留空则禁用跨域请求。
|
||||
allowed_origins: []
|
||||
# Allow credentials (cookies/authorization headers). Cannot be used with "*".
|
||||
# 允许携带凭证(cookies/授权头)。不能与 "*" 通配符同时使用。
|
||||
allow_credentials: true
|
||||
|
||||
# =============================================================================
|
||||
# Security Configuration
|
||||
# 安全配置
|
||||
# =============================================================================
|
||||
security:
|
||||
url_allowlist:
|
||||
# Enable URL allowlist validation (disable to skip all URL checks)
|
||||
# 启用 URL 白名单验证(禁用则跳过所有 URL 检查)
|
||||
enabled: false
|
||||
# Allowed upstream hosts for API proxying
|
||||
# 允许代理的上游 API 主机列表
|
||||
upstream_hosts:
|
||||
- "api.openai.com"
|
||||
- "api.anthropic.com"
|
||||
- "api.kimi.com"
|
||||
- "open.bigmodel.cn"
|
||||
- "api.minimaxi.com"
|
||||
- "generativelanguage.googleapis.com"
|
||||
- "cloudcode-pa.googleapis.com"
|
||||
- "*.openai.azure.com"
|
||||
# Allowed hosts for pricing data download
|
||||
# 允许下载定价数据的主机列表
|
||||
pricing_hosts:
|
||||
- "raw.githubusercontent.com"
|
||||
# Allowed hosts for CRS sync (required when using CRS sync)
|
||||
# 允许 CRS 同步的主机列表(使用 CRS 同步功能时必须配置)
|
||||
crs_hosts: []
|
||||
# Allow localhost/private IPs for upstream/pricing/CRS (use only in trusted networks)
|
||||
# 允许本地/私有 IP 地址用于上游/定价/CRS(仅在可信网络中使用)
|
||||
allow_private_hosts: true
|
||||
# Allow http:// URLs when allowlist is disabled (default: false, require https)
|
||||
# 白名单禁用时是否允许 http:// URL(默认: false,要求 https)
|
||||
allow_insecure_http: true
|
||||
response_headers:
|
||||
# Enable configurable response header filtering (disable to use default allowlist)
|
||||
# 启用可配置的响应头过滤(禁用则使用默认白名单)
|
||||
enabled: false
|
||||
# Extra allowed response headers from upstream
|
||||
# 额外允许的上游响应头
|
||||
additional_allowed: []
|
||||
# Force-remove response headers from upstream
|
||||
# 强制移除的上游响应头
|
||||
force_remove: []
|
||||
csp:
|
||||
# Enable Content-Security-Policy header
|
||||
# 启用内容安全策略 (CSP) 响应头
|
||||
enabled: true
|
||||
# Default CSP policy (override if you host assets on other domains)
|
||||
# 默认 CSP 策略(如果静态资源托管在其他域名,请自行覆盖)
|
||||
policy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
proxy_probe:
|
||||
# Allow skipping TLS verification for proxy probe (debug only)
|
||||
# 允许代理探测时跳过 TLS 证书验证(仅用于调试)
|
||||
insecure_skip_verify: false
|
||||
|
||||
# =============================================================================
|
||||
# Gateway Configuration
|
||||
# 网关配置
|
||||
# =============================================================================
|
||||
gateway:
|
||||
# Timeout for waiting upstream response headers (seconds)
|
||||
# 等待上游响应头超时时间(秒)
|
||||
response_header_timeout: 600
|
||||
# Max request body size in bytes (default: 100MB)
|
||||
# 请求体最大字节数(默认 100MB)
|
||||
max_body_size: 104857600
|
||||
# Connection pool isolation strategy:
|
||||
# 连接池隔离策略:
|
||||
# - proxy: Isolate by proxy, same proxy shares connection pool (suitable for few proxies, many accounts)
|
||||
# - proxy: 按代理隔离,同一代理共享连接池(适合代理少、账户多)
|
||||
# - account: Isolate by account, same account shares connection pool (suitable for few accounts, strict isolation)
|
||||
# - account: 按账户隔离,同一账户共享连接池(适合账户少、需严格隔离)
|
||||
# - account_proxy: Isolate by account+proxy combination (default, finest granularity)
|
||||
# - account_proxy: 按账户+代理组合隔离(默认,最细粒度)
|
||||
connection_pool_isolation: "account_proxy"
|
||||
# HTTP upstream connection pool settings (HTTP/2 + multi-proxy scenario defaults)
|
||||
# HTTP 上游连接池配置(HTTP/2 + 多代理场景默认值)
|
||||
# Max idle connections across all hosts
|
||||
# 所有主机的最大空闲连接数
|
||||
max_idle_conns: 240
|
||||
# Max idle connections per host
|
||||
# 每个主机的最大空闲连接数
|
||||
max_idle_conns_per_host: 120
|
||||
# Max connections per host
|
||||
# 每个主机的最大连接数
|
||||
max_conns_per_host: 240
|
||||
# Idle connection timeout (seconds)
|
||||
# 空闲连接超时时间(秒)
|
||||
idle_conn_timeout_seconds: 90
|
||||
# Upstream client cache settings
|
||||
# 上游连接池客户端缓存配置
|
||||
# max_upstream_clients: Max cached clients, evicts least recently used when exceeded
|
||||
# max_upstream_clients: 最大缓存客户端数量,超出后淘汰最久未使用的
|
||||
max_upstream_clients: 5000
|
||||
# client_idle_ttl_seconds: Client idle reclaim threshold (seconds), reclaimed when idle and no active requests
|
||||
# client_idle_ttl_seconds: 客户端空闲回收阈值(秒),超时且无活跃请求时回收
|
||||
client_idle_ttl_seconds: 900
|
||||
# Concurrency slot expiration time (minutes)
|
||||
# 并发槽位过期时间(分钟)
|
||||
concurrency_slot_ttl_minutes: 30
|
||||
# Stream data interval timeout (seconds), 0=disable
|
||||
# 流数据间隔超时(秒),0=禁用
|
||||
stream_data_interval_timeout: 180
|
||||
# Stream keepalive interval (seconds), 0=disable
|
||||
# 流式 keepalive 间隔(秒),0=禁用
|
||||
stream_keepalive_interval: 10
|
||||
# SSE max line size in bytes (default: 10MB)
|
||||
# SSE 单行最大字节数(默认 10MB)
|
||||
max_line_size: 10485760
|
||||
# Log upstream error response body summary (safe/truncated; does not log request content)
|
||||
# 记录上游错误响应体摘要(安全/截断;不记录请求内容)
|
||||
log_upstream_error_body: false
|
||||
# Max bytes to log from upstream error body
|
||||
# 记录上游错误响应体的最大字节数
|
||||
log_upstream_error_body_max_bytes: 2048
|
||||
# Auto inject anthropic-beta header for API-key accounts when needed (default: off)
|
||||
# 需要时自动为 API-key 账户注入 anthropic-beta 头(默认:关闭)
|
||||
inject_beta_for_apikey: false
|
||||
# Allow failover on selected 400 errors (default: off)
|
||||
# 允许在特定 400 错误时进行故障转移(默认:关闭)
|
||||
failover_on_400: false
|
||||
|
||||
# =============================================================================
|
||||
# Concurrency Wait Configuration
|
||||
# 并发等待配置
|
||||
# =============================================================================
|
||||
concurrency:
|
||||
# SSE ping interval during concurrency wait (seconds)
|
||||
# 并发等待期间的 SSE ping 间隔(秒)
|
||||
ping_interval: 10
|
||||
|
||||
# =============================================================================
|
||||
# Database Configuration (PostgreSQL)
|
||||
# 数据库配置 (PostgreSQL)
|
||||
# =============================================================================
|
||||
database:
|
||||
# Database host address
|
||||
# 数据库主机地址
|
||||
host: "localhost"
|
||||
# Database port
|
||||
# 数据库端口
|
||||
port: 5432
|
||||
# Database username
|
||||
# 数据库用户名
|
||||
user: "postgres"
|
||||
# Database password
|
||||
# 数据库密码
|
||||
password: "your_secure_password_here"
|
||||
# Database name
|
||||
# 数据库名称
|
||||
dbname: "sub2api"
|
||||
# SSL mode: disable, require, verify-ca, verify-full
|
||||
# SSL 模式:disable(禁用), require(要求), verify-ca(验证CA), verify-full(完全验证)
|
||||
sslmode: "disable"
|
||||
|
||||
# =============================================================================
|
||||
# Redis Configuration
|
||||
# Redis 配置
|
||||
# =============================================================================
|
||||
redis:
|
||||
# Redis host address
|
||||
# Redis 主机地址
|
||||
host: "localhost"
|
||||
# Redis port
|
||||
# Redis 端口
|
||||
port: 6379
|
||||
# Redis password (leave empty if no password is set)
|
||||
# Redis 密码(如果未设置密码则留空)
|
||||
password: ""
|
||||
# Database number (0-15)
|
||||
# 数据库编号(0-15)
|
||||
db: 0
|
||||
|
||||
# =============================================================================
|
||||
# JWT Configuration
|
||||
# JWT 配置
|
||||
# =============================================================================
|
||||
jwt:
|
||||
# IMPORTANT: Change this to a random string in production!
|
||||
# 重要:生产环境中请更改为随机字符串!
|
||||
# Generate with / 生成命令: openssl rand -hex 32
|
||||
secret: "change-this-to-a-secure-random-string"
|
||||
# Token expiration time in hours (max 24)
|
||||
# 令牌过期时间(小时,最大 24)
|
||||
expire_hour: 24
|
||||
|
||||
# =============================================================================
|
||||
# Default Settings
|
||||
# 默认设置
|
||||
# =============================================================================
|
||||
default:
|
||||
# Initial admin account (created on first run)
|
||||
# 初始管理员账户(首次运行时创建)
|
||||
admin_email: "admin@example.com"
|
||||
admin_password: "admin123"
|
||||
|
||||
# Default settings for new users
|
||||
# 新用户默认设置
|
||||
# Max concurrent requests per user
|
||||
# 每用户最大并发请求数
|
||||
user_concurrency: 5
|
||||
# Initial balance for new users
|
||||
# 新用户初始余额
|
||||
user_balance: 0
|
||||
|
||||
# API key settings
|
||||
# API 密钥设置
|
||||
# Prefix for generated API keys
|
||||
# 生成的 API 密钥前缀
|
||||
api_key_prefix: "sk-"
|
||||
|
||||
# Rate multiplier (affects billing calculation)
|
||||
# 费率倍数(影响计费计算)
|
||||
rate_multiplier: 1.0
|
||||
|
||||
# =============================================================================
|
||||
# Rate Limiting
|
||||
# 速率限制
|
||||
# =============================================================================
|
||||
rate_limit:
|
||||
# Cooldown time (in minutes) when upstream returns 529 (overloaded)
|
||||
# 上游返回 529(过载)时的冷却时间(分钟)
|
||||
overload_cooldown_minutes: 10
|
||||
|
||||
# =============================================================================
|
||||
# Pricing Data Source (Optional)
|
||||
# 定价数据源(可选)
|
||||
# =============================================================================
|
||||
pricing:
|
||||
# URL to fetch model pricing data (default: LiteLLM)
|
||||
# 获取模型定价数据的 URL(默认:LiteLLM)
|
||||
remote_url: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
|
||||
# Hash verification URL (optional)
|
||||
# 哈希校验 URL(可选)
|
||||
hash_url: ""
|
||||
# Local data directory for caching
|
||||
# 本地数据缓存目录
|
||||
data_dir: "./data"
|
||||
# Fallback pricing file
|
||||
# 备用定价文件
|
||||
fallback_file: "./resources/model-pricing/model_prices_and_context_window.json"
|
||||
# Update interval in hours
|
||||
# 更新间隔(小时)
|
||||
update_interval_hours: 24
|
||||
# Hash check interval in minutes
|
||||
# 哈希检查间隔(分钟)
|
||||
hash_check_interval_minutes: 10
|
||||
|
||||
# =============================================================================
|
||||
# Billing Configuration
|
||||
# 计费配置
|
||||
# =============================================================================
|
||||
billing:
|
||||
circuit_breaker:
|
||||
# Enable circuit breaker for billing service
|
||||
# 启用计费服务熔断器
|
||||
enabled: true
|
||||
# Number of failures before opening circuit
|
||||
# 触发熔断的失败次数阈值
|
||||
failure_threshold: 5
|
||||
# Time to wait before attempting reset (seconds)
|
||||
# 熔断后重试等待时间(秒)
|
||||
reset_timeout_seconds: 30
|
||||
# Number of requests to allow in half-open state
|
||||
# 半开状态允许通过的请求数
|
||||
half_open_requests: 3
|
||||
|
||||
# =============================================================================
|
||||
# Turnstile Configuration
|
||||
# Turnstile 人机验证配置
|
||||
# =============================================================================
|
||||
turnstile:
|
||||
# Require Turnstile in release mode (when enabled, login/register will fail if not configured)
|
||||
# 在 release 模式下要求 Turnstile 验证(启用后,若未配置则登录/注册会失败)
|
||||
required: false
|
||||
|
||||
# =============================================================================
|
||||
# Gemini OAuth (Required for Gemini accounts)
|
||||
# Gemini OAuth 配置(Gemini 账户必需)
|
||||
# =============================================================================
|
||||
# Sub2API supports TWO Gemini OAuth modes:
|
||||
# Sub2API 支持两种 Gemini OAuth 模式:
|
||||
#
|
||||
# 1. Code Assist OAuth (requires GCP project_id)
|
||||
# 1. Code Assist OAuth(需要 GCP project_id)
|
||||
# - Uses: cloudcode-pa.googleapis.com (Code Assist API)
|
||||
# - 使用:cloudcode-pa.googleapis.com(Code Assist API)
|
||||
#
|
||||
# 2. AI Studio OAuth (no project_id needed)
|
||||
# 2. AI Studio OAuth(不需要 project_id)
|
||||
# - Uses: generativelanguage.googleapis.com (AI Studio API)
|
||||
# - 使用:generativelanguage.googleapis.com(AI Studio API)
|
||||
#
|
||||
# Default: Uses Gemini CLI's public OAuth credentials (same as Google's official CLI tool)
|
||||
# 默认:使用 Gemini CLI 的公开 OAuth 凭证(与 Google 官方 CLI 工具相同)
|
||||
gemini:
|
||||
oauth:
|
||||
# Gemini CLI public OAuth credentials (works for both Code Assist and AI Studio)
|
||||
# Gemini CLI 公开 OAuth 凭证(适用于 Code Assist 和 AI Studio)
|
||||
client_id: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
|
||||
client_secret: "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"
|
||||
# Optional scopes (space-separated). Leave empty to auto-select based on oauth_type.
|
||||
# 可选的权限范围(空格分隔)。留空则根据 oauth_type 自动选择。
|
||||
scopes: ""
|
||||
quota:
|
||||
# Optional: local quota simulation for Gemini Code Assist (local billing).
|
||||
# 可选:Gemini Code Assist 本地配额模拟(本地计费)。
|
||||
# These values are used for UI progress + precheck scheduling, not official Google quotas.
|
||||
# 这些值用于 UI 进度显示和预检调度,并非 Google 官方配额。
|
||||
tiers:
|
||||
LEGACY:
|
||||
# Pro model requests per day
|
||||
# Pro 模型每日请求数
|
||||
pro_rpd: 50
|
||||
# Flash model requests per day
|
||||
# Flash 模型每日请求数
|
||||
flash_rpd: 1500
|
||||
# Cooldown time (minutes) after hitting quota
|
||||
# 达到配额后的冷却时间(分钟)
|
||||
cooldown_minutes: 30
|
||||
PRO:
|
||||
# Pro model requests per day
|
||||
# Pro 模型每日请求数
|
||||
pro_rpd: 1500
|
||||
# Flash model requests per day
|
||||
# Flash 模型每日请求数
|
||||
flash_rpd: 4000
|
||||
# Cooldown time (minutes) after hitting quota
|
||||
# 达到配额后的冷却时间(分钟)
|
||||
cooldown_minutes: 5
|
||||
ULTRA:
|
||||
# Pro model requests per day
|
||||
# Pro 模型每日请求数
|
||||
pro_rpd: 2000
|
||||
# Flash model requests per day (0 = unlimited)
|
||||
# Flash 模型每日请求数(0 = 无限制)
|
||||
flash_rpd: 0
|
||||
# Cooldown time (minutes) after hitting quota
|
||||
# 达到配额后的冷却时间(分钟)
|
||||
cooldown_minutes: 5
|
||||
@@ -66,6 +66,24 @@ JWT_EXPIRE_HOUR=24
|
||||
# Leave unset to use default ./config.yaml
|
||||
#CONFIG_FILE=./config.yaml
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Security Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
# URL Allowlist Configuration
|
||||
# 启用 URL 白名单验证(false 则跳过白名单检查,仅做基本格式校验)
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
|
||||
# 关闭白名单时,是否允许 http:// URL(默认 false,只允许 https://)
|
||||
# ⚠️ 警告:允许 HTTP 存在安全风险(明文传输),仅建议在开发/测试环境或可信内网中使用
|
||||
# Allow insecure HTTP URLs when allowlist is disabled (default: false, requires https)
|
||||
# ⚠️ WARNING: Allowing HTTP has security risks (plaintext transmission)
|
||||
# Only recommended for dev/test environments or trusted networks
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true
|
||||
|
||||
# 是否允许本地/私有 IP 地址用于上游/定价/CRS(仅在可信网络中使用)
|
||||
# Allow localhost/private IPs for upstream/pricing/CRS (use only in trusted networks)
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Gemini OAuth (OPTIONAL, required only for Gemini OAuth accounts)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -98,9 +98,14 @@ services:
|
||||
# =======================================================================
|
||||
# Security Configuration (URL Allowlist)
|
||||
# =======================================================================
|
||||
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
|
||||
# Allow private IP addresses for CRS sync (for internal deployments)
|
||||
# Enable URL allowlist validation (false to skip allowlist checks)
|
||||
- SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false}
|
||||
# Allow insecure HTTP URLs when allowlist is disabled (default: false, requires https)
|
||||
- SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-false}
|
||||
# Allow private IP addresses for upstream/pricing/CRS (for internal deployments)
|
||||
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
|
||||
# Upstream hosts whitelist (comma-separated, only used when enabled=true)
|
||||
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
58
docs/dependency-security.md
Normal file
58
docs/dependency-security.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Dependency Security
|
||||
|
||||
This document describes how dependency and toolchain security is managed in this repo.
|
||||
|
||||
## Go Toolchain Policy (Pinned to 1.25.5)
|
||||
|
||||
The Go toolchain is pinned to 1.25.5 to address known security issues.
|
||||
|
||||
Locations that MUST stay aligned:
|
||||
- `backend/go.mod`: `go 1.25.5` and `toolchain go1.25.5`
|
||||
- `Dockerfile`: `GOLANG_IMAGE=golang:1.25.5-alpine`
|
||||
- Workflows: use `go-version-file: backend/go.mod` and verify `go1.25.5`
|
||||
|
||||
Update process:
|
||||
1. Change `backend/go.mod` (go + toolchain) to the new patch version.
|
||||
2. Update `Dockerfile` GOLANG_IMAGE to the same patch version.
|
||||
3. Update workflows if needed and keep the `go version` check in place.
|
||||
4. Run `govulncheck` and the CI security scan workflow.
|
||||
|
||||
## Security Scans
|
||||
|
||||
Automated scans run via `.github/workflows/security-scan.yml`:
|
||||
- `govulncheck` for Go dependencies
|
||||
- `gosec` for static security issues
|
||||
- `pnpm audit` for frontend production dependencies
|
||||
|
||||
Policy:
|
||||
- High/Critical findings fail the build unless explicitly exempted.
|
||||
- Exemptions must include mitigation and an expiry date.
|
||||
|
||||
## Audit Exceptions
|
||||
|
||||
Exception list location: `.github/audit-exceptions.yml`
|
||||
|
||||
Required fields:
|
||||
- `package`
|
||||
- `advisory` (GHSA ID or advisory URL from pnpm audit)
|
||||
- `severity`
|
||||
- `mitigation`
|
||||
- `expires_on` (recommended <= 90 days)
|
||||
|
||||
Process:
|
||||
1. Add an exception with mitigation details and an expiry date.
|
||||
2. Ensure the exception is reviewed before expiry.
|
||||
3. Remove the exception when the dependency is upgraded or replaced.
|
||||
|
||||
## Frontend xlsx Mitigation (Plan A)
|
||||
|
||||
Current mitigation:
|
||||
- Use dynamic import so `xlsx` only loads during export.
|
||||
- Keep export access restricted and data scope limited.
|
||||
|
||||
## Rollback Guidance
|
||||
|
||||
If a change causes issues:
|
||||
- Go: revert `backend/go.mod` and `Dockerfile` to the previous version.
|
||||
- Frontend: revert the dynamic import change if needed.
|
||||
- CI: remove exception entries and re-run scans to confirm status.
|
||||
118
frontend/audit.json
Normal file
118
frontend/audit.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "review",
|
||||
"module": "xlsx",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 1108110,
|
||||
"path": ".>xlsx",
|
||||
"dev": false,
|
||||
"bundled": false,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"id": 1108111,
|
||||
"path": ".>xlsx",
|
||||
"dev": false,
|
||||
"bundled": false,
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"1108110": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "0.18.5",
|
||||
"paths": [
|
||||
".>xlsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"references": "- https://nvd.nist.gov/vuln/detail/CVE-2023-30533\n- https://cdn.sheetjs.com/advisories/CVE-2023-30533\n- https://git.sheetjs.com/sheetjs/sheetjs/src/branch/master/CHANGELOG.md\n- https://git.sheetjs.com/sheetjs/sheetjs/issues/2667\n- https://git.sheetjs.com/sheetjs/sheetjs/issues/2986\n- https://cdn.sheetjs.com\n- https://github.com/advisories/GHSA-4r6h-8v6p-xvw6",
|
||||
"created": "2023-04-24T09:30:19.000Z",
|
||||
"id": 1108110,
|
||||
"npm_advisory_id": null,
|
||||
"overview": "All versions of SheetJS CE through 0.19.2 are vulnerable to \"Prototype Pollution\" when reading specially crafted files. Workflows that do not read arbitrary files (for example, exporting data to spreadsheet files) are unaffected.\n\nA non-vulnerable version cannot be found via npm, as the repository hosted on GitHub and the npm package `xlsx` are no longer maintained. Version 0.19.3 can be downloaded via https://cdn.sheetjs.com/.",
|
||||
"reported_by": null,
|
||||
"title": "Prototype Pollution in sheetJS",
|
||||
"metadata": null,
|
||||
"cves": [
|
||||
"CVE-2023-30533"
|
||||
],
|
||||
"access": "public",
|
||||
"severity": "high",
|
||||
"module_name": "xlsx",
|
||||
"vulnerable_versions": "<0.19.3",
|
||||
"github_advisory_id": "GHSA-4r6h-8v6p-xvw6",
|
||||
"recommendation": "None",
|
||||
"patched_versions": "<0.0.0",
|
||||
"updated": "2025-09-19T15:23:41.000Z",
|
||||
"cvss": {
|
||||
"score": 7.8,
|
||||
"vectorString": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
|
||||
},
|
||||
"cwe": [
|
||||
"CWE-1321"
|
||||
],
|
||||
"url": "https://github.com/advisories/GHSA-4r6h-8v6p-xvw6"
|
||||
},
|
||||
"1108111": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "0.18.5",
|
||||
"paths": [
|
||||
".>xlsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"references": "- https://nvd.nist.gov/vuln/detail/CVE-2024-22363\n- https://cdn.sheetjs.com/advisories/CVE-2024-22363\n- https://cwe.mitre.org/data/definitions/1333.html\n- https://git.sheetjs.com/sheetjs/sheetjs/src/tag/v0.20.2\n- https://cdn.sheetjs.com\n- https://github.com/advisories/GHSA-5pgg-2g8v-p4x9",
|
||||
"created": "2024-04-05T06:30:46.000Z",
|
||||
"id": 1108111,
|
||||
"npm_advisory_id": null,
|
||||
"overview": "SheetJS Community Edition before 0.20.2 is vulnerable.to Regular Expression Denial of Service (ReDoS).\n\nA non-vulnerable version cannot be found via npm, as the repository hosted on GitHub and the npm package `xlsx` are no longer maintained. Version 0.20.2 can be downloaded via https://cdn.sheetjs.com/.",
|
||||
"reported_by": null,
|
||||
"title": "SheetJS Regular Expression Denial of Service (ReDoS)",
|
||||
"metadata": null,
|
||||
"cves": [
|
||||
"CVE-2024-22363"
|
||||
],
|
||||
"access": "public",
|
||||
"severity": "high",
|
||||
"module_name": "xlsx",
|
||||
"vulnerable_versions": "<0.20.2",
|
||||
"github_advisory_id": "GHSA-5pgg-2g8v-p4x9",
|
||||
"recommendation": "None",
|
||||
"patched_versions": "<0.0.0",
|
||||
"updated": "2025-09-19T15:23:26.000Z",
|
||||
"cvss": {
|
||||
"score": 7.5,
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
|
||||
},
|
||||
"cwe": [
|
||||
"CWE-1333"
|
||||
],
|
||||
"url": "https://github.com/advisories/GHSA-5pgg-2g8v-p4x9"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 2,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 639,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 639
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import * as XLSX from 'xlsx'; import { saveAs } from 'file-saver'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admin'; import { adminUsageAPI } from '@/api/admin/usage'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'; import Pagination from '@/components/common/Pagination.vue'
|
||||
import UsageStatsCards from '@/components/admin/usage/UsageStatsCards.vue'; import UsageFilters from '@/components/admin/usage/UsageFilters.vue'
|
||||
@@ -57,6 +57,8 @@ const exportToExcel = async () => {
|
||||
if (all.length >= total || res.items.length < 100) break; p++
|
||||
}
|
||||
if(!c.signal.aborted) {
|
||||
// 动态加载 xlsx,降低首屏包体并减少高危依赖的常驻暴露面。
|
||||
const XLSX = await import('xlsx')
|
||||
const ws = XLSX.utils.json_to_sheet(all); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Usage')
|
||||
saveAs(new Blob([XLSX.write(wb, { bookType: 'xlsx', type: 'array' })], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), `usage_${Date.now()}.xlsx`)
|
||||
appStore.showSuccess('Export Success')
|
||||
@@ -67,4 +69,4 @@ const exportToExcel = async () => {
|
||||
|
||||
onMounted(() => { loadLogs(); loadStats() })
|
||||
onUnmounted(() => { abortController?.abort(); exportAbortController?.abort() })
|
||||
</script>
|
||||
</script>
|
||||
|
||||
247
tools/check_pnpm_audit_exceptions.py
Normal file
247
tools/check_pnpm_audit_exceptions.py
Normal file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import date
|
||||
|
||||
|
||||
HIGH_SEVERITIES = {"high", "critical"}
|
||||
REQUIRED_FIELDS = {"package", "advisory", "severity", "mitigation", "expires_on"}
|
||||
|
||||
|
||||
def split_kv(line: str) -> tuple[str, str]:
|
||||
# 解析 "key: value" 形式的简单 YAML 行,并去除引号。
|
||||
key, value = line.split(":", 1)
|
||||
value = value.strip()
|
||||
if (value.startswith('"') and value.endswith('"')) or (
|
||||
value.startswith("'") and value.endswith("'")
|
||||
):
|
||||
value = value[1:-1]
|
||||
return key.strip(), value
|
||||
|
||||
|
||||
def parse_exceptions(path: str) -> list[dict]:
|
||||
# 轻量解析异常清单,避免引入额外依赖。
|
||||
exceptions = []
|
||||
current = None
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
for raw in handle:
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if line.startswith("version:") or line.startswith("exceptions:"):
|
||||
continue
|
||||
if line.startswith("- "):
|
||||
if current:
|
||||
exceptions.append(current)
|
||||
current = {}
|
||||
line = line[2:].strip()
|
||||
if line:
|
||||
key, value = split_kv(line)
|
||||
current[key] = value
|
||||
continue
|
||||
if current is not None and ":" in line:
|
||||
key, value = split_kv(line)
|
||||
current[key] = value
|
||||
if current:
|
||||
exceptions.append(current)
|
||||
return exceptions
|
||||
|
||||
|
||||
def pick_advisory_id(advisory: dict) -> str | None:
|
||||
# 优先使用可稳定匹配的标识(GHSA/URL/CVE),避免误匹配到其他同名漏洞。
|
||||
return (
|
||||
advisory.get("github_advisory_id")
|
||||
or advisory.get("url")
|
||||
or (advisory.get("cves") or [None])[0]
|
||||
or (str(advisory.get("id")) if advisory.get("id") is not None else None)
|
||||
or advisory.get("title")
|
||||
or advisory.get("advisory")
|
||||
or advisory.get("overview")
|
||||
)
|
||||
|
||||
|
||||
def iter_vulns(data: dict):
|
||||
# 兼容 pnpm audit 的不同输出结构(advisories / vulnerabilities),并提取 advisory 标识。
|
||||
advisories = data.get("advisories")
|
||||
if isinstance(advisories, dict):
|
||||
for advisory in advisories.values():
|
||||
name = advisory.get("module_name") or advisory.get("name")
|
||||
severity = advisory.get("severity")
|
||||
advisory_id = pick_advisory_id(advisory)
|
||||
title = (
|
||||
advisory.get("title")
|
||||
or advisory.get("advisory")
|
||||
or advisory.get("overview")
|
||||
or advisory.get("url")
|
||||
)
|
||||
yield name, severity, advisory_id, title
|
||||
|
||||
vulnerabilities = data.get("vulnerabilities")
|
||||
if isinstance(vulnerabilities, dict):
|
||||
for name, vuln in vulnerabilities.items():
|
||||
severity = vuln.get("severity")
|
||||
via = vuln.get("via", [])
|
||||
titles = []
|
||||
advisories = []
|
||||
if isinstance(via, list):
|
||||
for item in via:
|
||||
if isinstance(item, dict):
|
||||
advisories.append(
|
||||
item.get("github_advisory_id")
|
||||
or item.get("url")
|
||||
or item.get("source")
|
||||
or item.get("title")
|
||||
or item.get("name")
|
||||
)
|
||||
titles.append(
|
||||
item.get("title")
|
||||
or item.get("url")
|
||||
or item.get("advisory")
|
||||
or item.get("source")
|
||||
)
|
||||
elif isinstance(item, str):
|
||||
advisories.append(item)
|
||||
titles.append(item)
|
||||
elif isinstance(via, str):
|
||||
advisories.append(via)
|
||||
titles.append(via)
|
||||
title = "; ".join([t for t in titles if t])
|
||||
for advisory_id in [a for a in advisories if a]:
|
||||
yield name, severity, advisory_id, title
|
||||
|
||||
|
||||
def normalize_severity(severity: str) -> str:
|
||||
# 统一大小写,避免比较失败。
|
||||
return (severity or "").strip().lower()
|
||||
|
||||
|
||||
def normalize_package(name: str) -> str:
|
||||
# 包名只去掉首尾空白,保留原始大小写,同时兼容非字符串输入。
|
||||
if name is None:
|
||||
return ""
|
||||
return str(name).strip()
|
||||
|
||||
|
||||
def normalize_advisory(advisory: str) -> str:
|
||||
# advisory 统一为小写匹配,避免 GHSA/URL 因大小写差异导致漏匹配。
|
||||
# pnpm 的 source 字段可能是数字,这里统一转为字符串以保证可比较。
|
||||
if advisory is None:
|
||||
return ""
|
||||
return str(advisory).strip().lower()
|
||||
|
||||
|
||||
def parse_date(value: str) -> date | None:
|
||||
# 仅接受 ISO8601 日期格式,非法值视为无效。
|
||||
try:
|
||||
return date.fromisoformat(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--audit", required=True)
|
||||
parser.add_argument("--exceptions", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.audit, "r", encoding="utf-8") as handle:
|
||||
audit = json.load(handle)
|
||||
|
||||
# 读取异常清单并建立索引,便于快速匹配包名 + advisory。
|
||||
exceptions = parse_exceptions(args.exceptions)
|
||||
exception_index = {}
|
||||
errors = []
|
||||
|
||||
for exc in exceptions:
|
||||
missing = [field for field in REQUIRED_FIELDS if not exc.get(field)]
|
||||
if missing:
|
||||
errors.append(
|
||||
f"Exception missing required fields {missing}: {exc.get('package', '<unknown>')}"
|
||||
)
|
||||
continue
|
||||
exc_severity = normalize_severity(exc.get("severity"))
|
||||
exc_package = normalize_package(exc.get("package"))
|
||||
exc_advisory = normalize_advisory(exc.get("advisory"))
|
||||
exc_date = parse_date(exc.get("expires_on"))
|
||||
if exc_date is None:
|
||||
errors.append(
|
||||
f"Exception has invalid expires_on date: {exc.get('package', '<unknown>')}"
|
||||
)
|
||||
continue
|
||||
if not exc_package or not exc_advisory:
|
||||
errors.append("Exception missing package or advisory value")
|
||||
continue
|
||||
key = (exc_package, exc_advisory)
|
||||
if key in exception_index:
|
||||
errors.append(
|
||||
f"Duplicate exception for {exc_package} advisory {exc.get('advisory')}"
|
||||
)
|
||||
continue
|
||||
exception_index[key] = {
|
||||
"raw": exc,
|
||||
"severity": exc_severity,
|
||||
"expires_on": exc_date,
|
||||
}
|
||||
|
||||
today = date.today()
|
||||
missing_exceptions = []
|
||||
expired_exceptions = []
|
||||
|
||||
# 去重处理:同一包名 + advisory 可能在不同字段重复出现。
|
||||
seen = set()
|
||||
for name, severity, advisory_id, title in iter_vulns(audit):
|
||||
sev = normalize_severity(severity)
|
||||
if sev not in HIGH_SEVERITIES or not name:
|
||||
continue
|
||||
advisory_key = normalize_advisory(advisory_id)
|
||||
if not advisory_key:
|
||||
errors.append(
|
||||
f"High/Critical vulnerability missing advisory id: {name} ({sev})"
|
||||
)
|
||||
continue
|
||||
key = (normalize_package(name), advisory_key)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
exc = exception_index.get(key)
|
||||
if exc is None:
|
||||
missing_exceptions.append((name, sev, advisory_id, title))
|
||||
continue
|
||||
if exc["severity"] and exc["severity"] != sev:
|
||||
errors.append(
|
||||
"Exception severity mismatch: "
|
||||
f"{name} ({advisory_id}) expected {sev}, got {exc['severity']}"
|
||||
)
|
||||
if exc["expires_on"] and exc["expires_on"] < today:
|
||||
expired_exceptions.append(
|
||||
(name, sev, advisory_id, exc["expires_on"].isoformat())
|
||||
)
|
||||
|
||||
if missing_exceptions:
|
||||
errors.append("High/Critical vulnerabilities missing exceptions:")
|
||||
for name, sev, advisory_id, title in missing_exceptions:
|
||||
label = f"{name} ({sev})"
|
||||
if advisory_id:
|
||||
label = f"{label} [{advisory_id}]"
|
||||
if title:
|
||||
label = f"{label}: {title}"
|
||||
errors.append(f"- {label}")
|
||||
|
||||
if expired_exceptions:
|
||||
errors.append("Exceptions expired:")
|
||||
for name, sev, advisory_id, expires_on in expired_exceptions:
|
||||
errors.append(
|
||||
f"- {name} ({sev}) [{advisory_id}] expired on {expires_on}"
|
||||
)
|
||||
|
||||
if errors:
|
||||
sys.stderr.write("\n".join(errors) + "\n")
|
||||
return 1
|
||||
|
||||
print("Audit exceptions validated.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user