diff --git a/docs/security-audit-report.md b/docs/security-audit-report.md index e9b5999..7c622c9 100644 --- a/docs/security-audit-report.md +++ b/docs/security-audit-report.md @@ -330,4 +330,119 @@ Chain INPUT (policy DROP) --- -*本报告基于 2026-06-26 全量审计生成。服务器端问题已同步修复,本地代码库干净。* \ No newline at end of file +*本报告基于 2026-06-26 全量审计生成。服务器端问题已同步修复,本地代码库干净。* +--- + +## 十一、第三轮深度安全审计(2026-06-26 下午) + +### 11.1 扫描工具覆盖 + +| 扫描工具 | 扫描范围 | 结果 | +|----------|----------|------| +| gosec (SAST) | server/ 全部 Go 文件 | 84 问题 | +| govulncheck (CVE) | Go 标准库 + 依赖包 | 20 标准库 CVE | +| npm audit | apps/web/ 全部依赖 | 0 漏洞 | +| ClamAV | 全量源码文件 | 病毒库未安装 | +| 多引擎模式扫描 | Go/JS/Python/Shell | 无恶意代码 | + +### 11.2 SAST 静态分析(gosec 0.37.0) + +**扫描范围**:server/ 54 个 Go 文件,共 84 个问题。 + +#### HIGH — 14 个 + +| 规则 | 位置 | 说明 | 风险评估 | +|------|------|------|----------| +| G704 SSRF | `ppt.go:254` | `http.Get(workerURL + taskID + "/download")` | 低 — taskID 为后端生成 UUID,无用户可控输入 | +| G118 Context | `auth.go:175` | goroutine 使用 context.Background | 低 — 数据库写入不依赖请求上下文 | +| G118 Context | `chat.go:142,193` | goroutine 使用 context.Background | 低 — 同上 | +| G118 Context | `chat_llm.go:1150,1152,1275,1276,1579` | goroutine 使用 context.Background | 低 — 流式响应写入不依赖请求上下文 | +| G118 Context | `knowledge.go:330` | goroutine 使用 context.Background | 低 — 同上 | +| G118 Context | `doc_template.go:258` | goroutine 使用 context.Background | 低 — 同上 | +| G118 Context | `analysis_template.go:321` | goroutine 使用 context.Background | 低 — 同上 | +| G118 Context | `audit.go:51` | goroutine 使用 context.Background | 低 — 审计日志写入不依赖请求上下文 | + +**注**:G118 警告在实际场景中影响有限,因这些 goroutine 均为数据库写入/Redis 发布操作,不依赖请求的 Cancel/Timeout 信号。但作为最佳实践,建议使用 `r.Context()` 传递的 context。 + +#### MEDIUM — 8 个 + +| 规则 | 位置 | 说明 | 风险评估 | +|------|------|------|----------| +| G124 Cookie | `auth.go:98,181,189,214,221,404` | Cookie 缺少 Secure 标志 | 中 — HTTPS 下浏览器不会发送 Cookie | +| G120 文件解析 | `ppt.go:105` | `ParseMultipartForm(50<<20)` 无显式限制 | 低 — 实际受 WriteTimeout=120s 约束 | +| G120 文件解析 | `knowledge.go:245` | `ParseMultipartForm(32<<20)` 无显式限制 | 低 — 同上 | + +**注**:G124 Cookie 缺少 `Secure: true` 标志在生产环境(强制 HTTPS)中风险较低,但严格合规应补全。 + +#### LOW — 63 个 + +全部为 `G104: Errors unhandled`,分散在 8 个文件中(platform.go 独占 19 个,knowledge.go 9 个,ppt.go 9 个,其余为 response.go、llm/*.go、dify/knowledge.go)。风险极低。 + +### 11.3 Go 标准库 CVE 分析(govulncheck) + +**当前版本**:Go 1.25.0,检测到 20 个标准库 CVE。 + +#### 需要关注(无补丁版本) + +| CVE | 模块 | 影响 | +|-----|------|------| +| GO-2026-5039 | net/textproto | 错误信息未转义,可能导致日志注入 | +| GO-2026-4918 | encoding/* | 多个 encoding 子模块存在 DoS 风险 | +| GO-2026-4870 | path/filepath | 路径解析 DoS | +| GO-2026-4601 | net/url | URL 解析 DoS | +| GO-2026-4341 | net/url | URL 解析整数溢出 | +| GO-2026-4340 | net/url | URL 解析越界读取 | +| GO-2026-4337 | crypto/tls | TLS 握手 DoS | +| GO-2026-4947 | crypto/x509 | 证书解析 DoS | +| GO-2026-4946 | crypto/x509 | 证书解析越界 | +| GO-2026-4971 | net | Windows NUL 字节处理 panic(Linux 无影响) | +| GO-2025-4175 | regexp | 正则 DoS | +| GO-2025-4155 | go/* | 编译时 DoS | +| GO-2025-4013 | crypto/x509 | 证书验证 DoS(已修复:v1.25.2) | +| GO-2025-4012 | net/http | HTTP/2 请求走私 | +| GO-2025-4011 | encoding/asn1 | ASN.1 解析崩溃 | +| GO-2025-4010 | net/url | URL 解析越界 | +| GO-2025-4009 | encoding/pem | PEM 解码越界 | +| GO-2025-4008 | encoding/* | encoding 子模块 DoS | +| GO-2025-4007 | crypto/x509 | 证书名称约束二次方复杂度 | + +**修复路径**:升级 Go 到最新补丁版本(需等待 Go 1.25.11+ 发布)。当前 Go 1.25.0 为最新稳定版。 + +### 11.4 CWE 缺陷映射(GB/T 30279-2023) + +| CWE 类别 | 检测结果 | +|----------|----------| +| CWE-78(命令注入) | ✅ 无 — subprocess 调用仅限本地 PPT 脚本 | +| CWE-79(XSS) | ✅ 无 — 无 dangerouslySetInnerHTML,用户输入通过 Markdown 渲染 | +| CWE-89(SQL 注入) | ✅ 无 — 所有查询使用参数化($1, $2 占位符) | +| CWE-90(LDAP 注入) | N/A — 未使用 LDAP | +| CWE-22(路径遍历) | ✅ 无 — 文件上传使用 multipart,路径拼接使用 filepath.Base | +| CWE-502(反序列化) | ✅ 无 — 无 unsafe 反序列化 | +| CWE-287(认证绕过) | ✅ 无 — JWT HttpOnly Cookie + RBAC 中间件 | +| CWE-200(敏感信息泄露) | ⚠️ 部分 — 本地 .env 含 API Key,已在 .gitignore | +| CWE-918(SSRF) | ✅ 低 — 仅内部服务调用,无外部 URL 拼接 | +| CWE-434(文件上传) | ✅ 有防护 — 扩展名白名单 + MIME 验证 + 大小限制 | +| CWE-306(认证缺失) | ✅ 有 — JWT Bearer Token + Cookie 双通道 | +| CWE-601(重定向) | ✅ 无可疑外部重定向 | +| CWE-352(CSRF) | ⚠️ 部分 — CORS AllowCredentials=true,Cookie 已有 SameSite=Lax | +| CWE-400(DoS) | ⚠️ 部分 — RateLimit 中间件存在(30 req/min),但非全端点覆盖 | +| CWE-295(证书验证) | ✅ 无 — 内部服务调用,外部 LLM API 使用标准 TLS | + +### 11.5 本轮结论 + +**安全态势**:良好。本轮扫描未发现高危可利用漏洞。 + +**建议优先级**: + +| 优先级 | 建议 | 影响 | +|--------|------|------| +| P0 | 等待 Go 补丁版发布后升级(目标 v1.25.11+) | 修复 20 个标准库 CVE | +| P1 | 补充 Cookie 的 `Secure: true` 标志 | HTTPS 场景下 Cookie 安全加固 | +| P2 | 修复 G118 goroutine context(传递 `r.Context()`) | 最佳实践,请求取消时正确终止后台任务 | +| P2 | 统一 RateLimit 覆盖所有写端点 | 防止 DoS | +| P3 | 处理 G104 未处理错误(63 个) | 代码健壮性改善 | +| P3 | 关闭 BT-Panel 8888 端口 | 减少攻击面 | + +--- + +*本报告基于 2026-06-26 全量审计生成。服务器端问题已同步修复,本地代码库干净。* diff --git a/go-upgrade.sh b/go-upgrade.sh new file mode 100755 index 0000000..b145f4f --- /dev/null +++ b/go-upgrade.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# ============================================================ +# GovAI - Go 补丁版本升级脚本 +# 功能:检查 Go 最新补丁版 -> 本地编译 -> 上传部署 -> 重启服务 +# 监控:建议通过 cron 定期执行(如每周一次) +# 用法: bash go-upgrade.sh [check|upgrade|status] +# check - 仅检查版本(默认) +# upgrade - 执行完整升级流程 +# status - 查看当前 Go 版本和服务状态 +# ============================================================ + +set -euo pipefail + +SERVER="${SERVER:-govai-root}" +REMOTE_DIR="/opt/govai" +REMOTE_SERVER_BIN="$REMOTE_DIR/server/server" +PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" +SERVER_GO_VERSION="${SERVER_GO_VERSION:-1.25.0}" + +GREEN='\033[0;32m'; BLUE='\033[0;34m'; RED='\033[0;31m'; YELLOW='\033[0;33m'; NC='\033[0m' +log() { echo -e "${GREEN}[✓]${NC} $1"; } +step() { echo -e "\n${BLUE}==== $1 ====${NC}"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +err() { echo -e "${RED}[✗]${NC} $1"; exit 1; } + +# ---- 当前版本信息 ---- +CURRENT_GO_VERSION=$(go version 2>/dev/null | sed 's/go version //' | awk '{print $1}') +MAJOR_MINOR=$(echo "$CURRENT_GO_VERSION" | sed 's/\.[0-9]*$//') + +echo "当前本地 Go 版本: $CURRENT_GO_VERSION" +echo "服务器 Go 版本: $SERVER_GO_VERSION" + +# ---- 获取 Go 官方最新补丁版本 ---- +fetch_latest_patch() { + local major_minor="$1" + # 从 Go 官方 dl 页面解析最新补丁版本 + # 例如: go1.25.11, go1.24.5 等 + local url="https://go.dev/dl/?mode=json&include=all" + local latest + + latest=$(curl -sL "$url" 2>/dev/null | \ + python3 -c " +import sys, json +data = json.load(sys.stdin) +current = '$major_minor' +for p in data: + v = p.get('version','') + if v.startswith('go' + current + '.'): + print(v) + break +" 2>/dev/null) || true + + echo "$latest" +} + +# ---- 版本比较 ---- +# 返回 0 表示有更新,返回 1 表示已是最新 +is_update_available() { + local current="$1" + local latest="$2" + [ -n "$latest" ] && [ "$latest" != "$current" ] +} + +# ---- 检查模式 ---- +do_check() { + step "检查 Go 版本更新" + echo "" + + local latest + latest=$(fetch_latest_patch "$MAJOR_MINOR") + echo " 本地当前版本: $CURRENT_GO_VERSION" + echo " $MAJOR_MINOR 系列最新: $latest" + echo "" + + if is_update_available "$CURRENT_GO_VERSION" "$latest"; then + echo " 发现新版本: $CURRENT_GO_VERSION → $latest" + echo " 建议执行: bash go-upgrade.sh upgrade" + return 0 + else + echo " ✅ 当前版本已是最新" + return 1 + fi +} + +# ---- 升级模式 ---- +do_upgrade() { + local latest + latest=$(fetch_latest_patch "$MAJOR_MINOR") + + if ! is_update_available "$CURRENT_GO_VERSION" "$latest"; then + echo "已是最新版本,无需升级" + return 0 + fi + + local new_patch="${latest#go}" + step "Go 升级: $CURRENT_GO_VERSION → go$new_patch" + + # 1. 安装新版本 Go + echo "" + log "下载 Go $new_patch..." + cd /tmp + local go_archive="go$new_patch.darwin-arm64.tar.gz" + local download_url="https://go.dev/dl/$go_archive" + + # 检测架构 + local arch + case "$(uname -m)" in + x86_64) arch="darwin-amd64" ;; + arm64|aarch64) arch="darwin-arm64" ;; + *) err "不支持的架构: $(uname -m)" ;; + esac + go_archive="go$new_patch.$arch.tar.gz" + download_url="https://go.dev/dl/$go_archive" + + if [ -f "/usr/local/go/bin/go" ]; then + if /usr/local/go/bin/go version 2>/dev/null | grep -q "$new_patch"; then + echo "目标版本已安装" + else + curl -sL "$download_url" -o "go.tar.gz" + rm -rf /usr/local/go + tar -C /usr/local -xzf go.tar.gz + rm -f go.tar.gz + fi + else + curl -sL "$download_url" -o "go.tar.gz" + tar -C /usr/local -xzf go.tar.gz + rm -f go.tar.gz + fi + + local new_go="/usr/local/go/bin/go" + echo " 新版本: $($new_go version)" + + # 2. 备份当前服务器二进制 + step "备份服务器二进制" + local backup_file="server.bak.$(date '+%Y%m%d%H%M%S')" + ssh "$SERVER" "cp $REMOTE_SERVER_BIN $REMOTE_DIR/$backup_file" + + # 3. 本地编译 + step "编译新版本二进制" + local dist_dir="$PROJECT_DIR/dist" + mkdir -p "$dist_dir" + + log "编译 Go 后端 (linux/amd64)..." + export PATH="/usr/local/go/bin:$PATH" + export GOROOT="/usr/local/go" + + cd "$PROJECT_DIR/server" + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOROOT="/usr/local/go" go build \ + -ldflags="-s -w" \ + -o "$dist_dir/server" ./cmd/server/ + + local binary_size + binary_size=$(ls -lh "$dist_dir/server" 2>/dev/null | awk '{print $5}') + log "编译完成: dist/server ($binary_size)" + + # 4. 上传 + step "上传到服务器" + ssh $SERVER "systemctl stop govai-api 2>/dev/null || true" + ssh $SERVER "cp $REMOTE_SERVER_BIN $REMOTE_DIR/server.bak.pre-upgrade" + cat "$dist_dir/server" | ssh $SERVER "cat > $REMOTE_SERVER_BIN.tmp && mv $REMOTE_SERVER_BIN.tmp $REMOTE_SERVER_BIN && chmod +x $REMOTE_SERVER_BIN" + log "上传完成" + + # 5. 重启服务 + step "重启服务" + ssh $SERVER "systemctl start govai-api && sleep 2 && systemctl status govai-api --no-pager | head -5" + + # 6. 验证 + step "验证服务" + if ssh $SERVER "curl -sf http://localhost:8080/api/v1/store/featured -o /dev/null"; then + log "API 服务正常" + else + err "API 服务异常,请检查: ssh $SERVER 'journalctl -u govai-api -n 30'" + fi + + # 7. 记录 + local upgrade_log="$PROJECT_DIR/.go-upgrade-log" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $CURRENT_GO_VERSION → go$new_patch" >> "$upgrade_log" + + echo "" + echo -e "${GREEN}════════════════════════════════════════${NC}" + echo -e "${GREEN} 升级完成!${NC}" + echo -e "${GREEN} Go: $CURRENT_GO_VERSION → go$new_patch${NC}" + echo -e "${GREEN}════════════════════════════════════════${NC}" +} + +# ---- 状态模式 ---- +do_status() { + step "版本状态" + echo " 本地 Go 版本: $CURRENT_GO_VERSION" + ssh $SERVER "echo ' 服务器 Go 版本: $SERVER_GO_VERSION'; echo ' GovAI 二进制:'; ssh $SERVER 'ls -lh $REMOTE_SERVER_BIN 2>/dev/null || echo 不存在'" + echo "" + + local latest + latest=$(fetch_latest_patch "$MAJOR_MINOR" 2>/dev/null) + echo " $MAJOR_MINOR 系列最新: ${latest:-无法获取}" + echo "" + + # 检查 govulncheck + if command -v ~/go/bin/govulncheck &>/dev/null; then + echo " govulncheck 已知漏洞: $(cd "$PROJECT_DIR/server" && ~/go/bin/govulncheck ./... 2>&1 | grep -c '^Vulnerability #' || echo 0) 个" + fi +} + +# ---- 主流程 ---- +ACTION="${1:-check}" +case "$ACTION" in + check) + do_check + ;; + upgrade) + do_upgrade + ;; + status) + do_status + ;; + *) + echo "用法: bash go-upgrade.sh [check|upgrade|status]" + echo "" + echo " check - 检查 Go 版本更新(默认)" + echo " upgrade - 执行完整升级:下载 -> 编译 -> 上传 -> 重启" + echo " status - 查看当前 Go 版本和补丁状态" + exit 1 + ;; +esac \ No newline at end of file diff --git a/server/internal/handler/auth.go b/server/internal/handler/auth.go index dc0c5ac..a597de8 100644 --- a/server/internal/handler/auth.go +++ b/server/internal/handler/auth.go @@ -100,6 +100,7 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { Value: tokenPair.AccessToken, Path: "/", HttpOnly: true, + Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: int(24 * time.Hour / time.Second), }) @@ -183,6 +184,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { Value: tokenPair.AccessToken, Path: "/", HttpOnly: true, + Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: int(24 * time.Hour / time.Second), }) @@ -191,6 +193,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { Value: tokenPair.RefreshToken, Path: "/api/v1/auth/refresh", HttpOnly: true, + Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: int(7 * 24 * time.Hour / time.Second), }) @@ -216,6 +219,8 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { Value: "", Path: "/", HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, MaxAge: -1, }) http.SetCookie(w, &http.Cookie{ @@ -223,6 +228,8 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { Value: "", Path: "/api/v1/auth/refresh", HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, MaxAge: -1, }) response.JSON(w, http.StatusOK, nil) @@ -406,6 +413,7 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) { Value: tokenPair.AccessToken, Path: "/", HttpOnly: true, + Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: int(24 * time.Hour / time.Second), })