diff --git a/ai_review/agent/review_agent.py b/ai_review/agent/review_agent.py index b83b21a..0333854 100644 --- a/ai_review/agent/review_agent.py +++ b/ai_review/agent/review_agent.py @@ -20,8 +20,8 @@ SYSTEM_PROMPT = """You are a strict content moderation AI. Your task is to revie ## Output format: Return ONLY a valid JSON object, no other text. -For violation: {{"invalid": "true"}} -For safe content: {{"invalid": "false"}} +For violation: {{"invalid": true}} +For safe content: {{"invalid": false}} ## Reference examples: {examples} @@ -42,7 +42,7 @@ class ReviewAgent: base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"), ) self.rag = RAGStore() - self.model = os.getenv("DEEPSEEK_MODEL", "deepseek-chat") + self.model = os.getenv("DEEPSEEK_MODEL", "deepseek-v4-flash") self.temperature = float(os.getenv("DEEPSEEK_TEMPERATURE", "0.0")) self.max_tokens = int(os.getenv("DEEPSEEK_MAX_TOKENS", "128")) @@ -68,7 +68,7 @@ class ReviewAgent: ) raw = response.choices[0].message.content.strip() except Exception as e: - return {"invalid": "false", "error": "api_error", "detail": str(e)} + return {"invalid": "true", "error": "api_error", "detail": str(e)} try: result = json.loads(raw) @@ -77,8 +77,8 @@ class ReviewAgent: if '"invalid"' in raw_lower and 'true' in raw_lower: result = {"invalid": "true"} elif '"invalid"' in raw_lower and 'false' in raw_lower: - result = {"invalid": "false"} + result = {"invalid": "true"} else: - result = {"invalid": "false", "error": "parse_failed", "raw": raw} + result = {"invalid": "true", "error": "parse_failed", "raw": raw} return result diff --git a/captcha/_run.sh b/captcha/_run.sh index 574e884..b4c1664 100755 --- a/captcha/_run.sh +++ b/captcha/_run.sh @@ -1,3 +1,3 @@ #!/bin/bash cd "$(dirname "$0")" || exit -python3 captcha_service.py +exec python3 captcha_service.py diff --git a/dnfs/dnfs.py b/dnfs/dnfs.py index 8772eb6..fdae19f 100755 --- a/dnfs/dnfs.py +++ b/dnfs/dnfs.py @@ -100,7 +100,11 @@ def review_content(content: str, timeout: int = 10) -> Dict[str, Any]: with urllib.request.urlopen(req, timeout=timeout) as resp: resp_data = resp.read().decode("utf-8") data = json.loads(resp_data) - return not data['invalid'] + invalid: bool = data['invalid'] + return invalid == False + #invalid = data + #return invalid + except urllib.error.HTTPError as e: # 服务端返回 4xx/5xx 错误时,尝试解析错误正文 error_body = e.read().decode("utf-8") @@ -197,8 +201,9 @@ def add_note(): # 内容审查 - review = review_content(title + '\n\n' + content, 10) - if not review: + review = review_content(title + '\n' + content, 10) + if True != review: + #return jsonify({'error': 'review failed', 'data': review}), 400 return jsonify({'error': 'review failed'}), 400 insert_payload = { @@ -351,6 +356,29 @@ def update_note(): return jsonify({"error": "Invalid param"}), 400 + if pid != 0: + + select_payload = { + 'action': 'select', + 'table': 'Notes', + 'where': { + 'id': pid + } + } + + notes = call_db_service(DB_SERVER_URL, select_payload) + + # 不存在的父节点 + if len( notes['data']) == 0: + return jsonify({'error': 'the new node need a parent'}), 400 + + + # 内容审查 + review = review_content(title + '\n' + content, 10) + if True != review: + #return jsonify({'error': 'review failed', 'data': review}), 400 + return jsonify({'error': 'review failed'}), 400 + #with app.app_context(): # note = Note.query.get(id) diff --git a/gateway/inc/ExpBase.hpp b/gateway/inc/ExpBase.hpp index 8531cf6..c2509c6 100644 --- a/gateway/inc/ExpBase.hpp +++ b/gateway/inc/ExpBase.hpp @@ -161,15 +161,23 @@ protected: virtual std::string build_message() const { std::string result; - + // 错误码 if (m_code != 0) { result += "[Error " + std::to_string(m_code) + "] "; } - + // 错误消息 result += m_message; - + + /* ===== 以下「位置信息」与「嵌套异常」暂时以注释方式保留 ===== + * 原因:黑盒检测发现 what() 的返回值会透传到 HTTP 响应体, + * 导致泄露源码文件路径、行号、C++ 模板类型签名(函数名), + * 便于攻击者定位代码、确认技术栈、构造针对性 payload(P0-3)。 + * 排障:完整信息仍由各 handler 通过 std::cerr 输出到服务端日志。 + * 恢复:如需调试可临时取消下方注释;生产环境必须保持注释状态, + * 并确保 what() 不直接进入客户端响应。 + * ==================================================================== // 位置信息 if (!m_file.empty()) { result += " (at " + m_file; @@ -181,12 +189,13 @@ protected: } result += ")"; } - + // 嵌套异常 if (m_nested) { result += "\n Caused by: " + std::string(m_nested->what()); } - + * ==================================================================== */ + return result; } diff --git a/gateway/inc/LocalTime.hpp b/gateway/inc/LocalTime.hpp index ca5cd42..be7d415 100644 --- a/gateway/inc/LocalTime.hpp +++ b/gateway/inc/LocalTime.hpp @@ -1,3 +1,4 @@ +#pragma once #include #include #include @@ -13,7 +14,7 @@ public: auto now = system_clock::now(); auto zone = current_zone(); zoned_time zt{zone, now}; - return std::vformat(format, std::make_format_args(zt)); + return std::vformat("{:" + format + "}", std::make_format_args(zt)); } // 解析时间字符串为 system_clock::time_point(本地时间 → UTC) diff --git a/gateway/src/main.cpp b/gateway/src/main.cpp index 7a90b26..9a0ae2d 100644 --- a/gateway/src/main.cpp +++ b/gateway/src/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -151,9 +152,12 @@ public: bool has_upper = false, has_lower = false, has_digit = false, has_special = false; for (char c : password) { - if (std::isupper(c)) has_upper = true; - else if (std::islower(c)) has_lower = true; - else if (std::isdigit(c)) has_digit = true; + // std::isupper 等函数参数为 int,char 为有符号且非 ASCII 时是 UB + // 必须 static_cast 后再传入(P1-2) + unsigned char uc = static_cast(c); + if (std::isupper(uc)) has_upper = true; + else if (std::islower(uc)) has_lower = true; + else if (std::isdigit(uc)) has_digit = true; else has_special = true; } @@ -277,6 +281,9 @@ http::response handleCaptcha( /* headers can't processed */ bool is_special_header(const std::string &header); +/* 请求路径安全性校验(防路径遍历/协议注入/空字节) */ +bool is_safe_target(const std::string& target); + /* configure items */ static std::string _ADDR; static int _PORT; @@ -304,8 +311,10 @@ static std::mutex _CAPTCHA_MTX; std::string generate_captcha_text() { static const char chars[] = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; - static std::mt19937 rng(std::random_device{}()); - static std::uniform_int_distribution dist(0, sizeof(chars) - 2); + // thread_local:每个线程独立 rng 实例,避免多线程并发调用 dist(rng) 时的数据竞争 + // (static 变量初始化本身线程安全,但 mt19937::operator() 修改内部状态非线程安全) + thread_local std::mt19937 rng(std::random_device{}()); + thread_local std::uniform_int_distribution dist(0, sizeof(chars) - 2); std::string text; for (int i = 0; i < 5; i++) { @@ -497,7 +506,28 @@ void registerRoutes(HttpServer &server) { server.options("*", [](const auto& req, const auto& params) { pmc::net::http::response res; res.result(pmc::net::http::status::ok); - res.set(pmc::net::http::field::access_control_allow_origin, "*"); + + // CORS 白名单(P1-4):仅允许官方域名,防止任意网站跨域调用 + // 原配置 Access-Control-Allow-Origin: * 过宽,XSS 拿到 token 后可跨域读取数据 + // 动态回填 Origin:仅当请求 Origin 命中白名单时才返回该值,否则不返回 ACAO 头 + static const std::vector allowed_origins = { + "https://qsont.me", + "https://www.qsont.me", + "http://127.0.0.1:8443", // 本地开发调试 + "http://localhost:8443" + }; + auto origin_it = req.base().find("Origin"); + if (origin_it != req.base().end()) { + std::string origin = std::string(origin_it->value()); + for (const auto& allowed : allowed_origins) { + if (origin == allowed) { + res.set(pmc::net::http::field::access_control_allow_origin, origin); + res.set(pmc::net::http::field::vary, "Origin"); + break; + } + } + } + res.set(pmc::net::http::field::access_control_allow_methods, "GET, POST, OPTIONS"); res.set(pmc::net::http::field::access_control_allow_headers, "Content-Type, Authorization"); res.set(pmc::net::http::field::access_control_max_age, "86400"); @@ -523,11 +553,17 @@ catch (const std::exception& e) { http::response res; res.version(req.version()); const auto& ti = typeid(e); + // 详细错误信息(ti.name() 含 mangled 类型名、e.what() 含内部细节)暂时以注释方式保留 + // 原因:此处为 wrapperHttp 外层 catch,会捕获 handler 内未处理的 std::exception + // 透传到客户端会泄露 C++ 类型信息与内部路径(P0-3 遗漏点,已补全) + // 排障:完整信息仍输出到服务端 stderr + std::cerr << "[wrapperHttp std::exception] <" << ti.name() << "> " << e.what() << std::endl; json::value jv_r = { {"success", false}, - {"message", std::string("<") + ti.name() + "> " + e.what()} + /* {"message", std::string("<") + ti.name() + "> " + e.what()}, */ // 泄露源码信息,已禁用 + {"message", "Internal Server Error"} }; - res.set(http::field::content_type, "text/plain"); + res.set(http::field::content_type, "application/json"); res.result(http::status::internal_server_error); res.body() = json::serialize(jv_r); res.prepare_payload(); @@ -695,9 +731,14 @@ http::response handleLogin( catch (const HttpException& e) { + // 详细错误信息(e.what() 含源码路径/行号/C++ 模板类型)暂时以注释方式保留 + // 原因:黑盒检测发现透传到客户端会泄露内部实现,便于攻击者定位代码(P0-3) + // 排障:完整信息仍输出到服务端 stderr + std::cerr << "[handleLogin HttpException] " << e.what() << std::endl; json::value jv_r = { {"success", false}, - {"message", e.what()} + /* {"message", e.what()}, */ // 泄露源码信息,已禁用 + {"message", "Invalid Username or Password"} }; res.set(http::field::content_type, "application/json"); res.result(e.status()); @@ -717,6 +758,10 @@ http::response handleRegister( // IP rate limit check std::string client_ip = "direct"; + auto it_host = req.base().find("X-Forwarded-For"); + if (it_host != req.base().end()) { + client_ip = std::string(it_host->value()); + } if (!_REGISTER_RATE.check_and_record(client_ip, 3, 600)) { json::value jv_r = { {"success", false}, @@ -793,9 +838,12 @@ http::response handleRegister( } catch (const HttpException& e) { + // 详细错误信息(e.what() 含源码路径/行号/C++ 模板类型)暂时以注释方式保留(同 handleLogin) + std::cerr << "[handleRegister HttpException] " << e.what() << std::endl; json::value jv_r = { {"success", false}, - {"message", e.what()} + /* {"message", e.what()}, */ // 泄露源码信息,已禁用 + {"message", "Registration failed"} }; res.set(http::field::content_type, "application/json"); res.result(e.status()); @@ -804,9 +852,19 @@ http::response handleRegister( catch (const std::exception& e) { const auto& ti = typeid(e); - res.set(http::field::content_type, "text/plain"); + // 详细错误信息(ti.name() 含 mangled 类型名、e.what() 含内部细节)暂时以注释方式保留 + // 原因:mangled 类型名会确认技术栈与 STL 容器,e.what() 可能泄露内部路径(P0-3) + // 排障:完整信息仍输出到服务端 stderr + std::cerr << "[handleRegister std::exception] <" << ti.name() << "> " << e.what() << std::endl; + res.set(http::field::content_type, "application/json"); res.result(http::status::internal_server_error); - res.body() = std::string("<") + ti.name() + "> " + e.what(); + json::value jv_r = { + {"success", false}, + /* {"err_name", ti.name()}, // 泄露 C++ 类型信息,已禁用 */ + /* {"err_message", e.what()} // 泄露内部细节,已禁用 */ + {"message", "Internal Server Error"} + }; + res.body() = json::serialize(jv_r); } res.prepare_payload(); @@ -845,9 +903,17 @@ http::response handleProxy( flag = true; } + // 路径规范化校验(P1-1):防止路径遍历、协议注入、空字节等 + // 校验在 url 拼接前完成,避免恶意 target 污染下游请求 + std::string target_path = std::string(req.target()); + if (!is_safe_target(target_path)) { + THROW_EX_CUSTOM(HttpException, 2300, "Invalid request path", + 400, "*"); + } + url = t_url; url += "/"; - url += req.target(); + url += target_path; it = req.base().find("Authorization"); if (it == req.base().end() && !flag) { @@ -915,6 +981,18 @@ http::response handleProxy( client.set_header(h); } + // 主动设置可信的 X-Forwarded-For(P1-1) + // 说明:is_special_header 已剥离客户端可能伪造的 XFF/X-Real-IP/Forwarded + // WAF → Gateway 为可信内网链路,收到的 XFF 即客户端真实 IP + // 下游服务应信任此值做限流/审计,不再被客户端伪造 + auto xff_it = req.base().find("X-Forwarded-For"); + if (xff_it != req.base().end()) { + client.set_header(std::string("X-Forwarded-For: ") + std::string(xff_it->value())); + } else { + // WAF 未传 XFF 时,标记为 unknown,避免下游拿到空值 + client.set_header("X-Forwarded-For: unknown"); + } + if (method == "get") client.get_stream_sync(url, callback); @@ -934,12 +1012,17 @@ http::response handleProxy( catch (const std::exception& e) { const auto& ti = typeid(e); - res.set(http::field::content_type, "text/plain"); + // 详细错误信息(ti.name() 含 mangled 类型名、e.what() 含内部细节)暂时以注释方式保留 + // 原因:mangled 类型名会确认技术栈与 STL 容器,e.what() 可能泄露内部路径(P0-3) + // 排障:完整信息仍输出到服务端 stderr + std::cerr << "[handleProxy std::exception] <" << ti.name() << "> " << e.what() << std::endl; + res.set(http::field::content_type, "application/json"); res.result(http::status::internal_server_error); json::value jv_r = { {"success", false}, - {"err_name", ti.name()}, - {"err_message", e.what()} + /* {"err_name", ti.name()}, // 泄露 C++ 类型信息,已禁用 */ + /* {"err_message", e.what()} // 泄露内部细节,已禁用 */ + {"message", "Internal Server Error"} }; res.body() = json::serialize(jv_r); } @@ -950,6 +1033,42 @@ http::response handleProxy( bool is_special_header(const std::string &header) { + // 原过滤:传输层/编码相关,由 HttpClient 自行处理 + // 新增过滤(P1-1):客户端可伪造的 IP 相关头 + // - X-Forwarded-For / X-Real-IP / Forwarded + // 剥离后由 gateway 主动设置可信值(见 handleProxy 转发逻辑) + // 防止客户端伪造 IP 绕过下游限流/审计 return header == "Host" || header == "connection" - || header == "Accept-Encoding"; + || header == "Accept-Encoding" + || header == "X-Forwarded-For" + || header == "X-Real-IP" + || header == "Forwarded"; +} + +bool is_safe_target(const std::string& target) { + // 请求路径安全性校验(P1-1) + // 防御目标:路径遍历、协议注入、空字节、非绝对路径形式 + + if (target.empty()) return false; + + // 必须以 '/' 开头(拒绝 "http://evil.com/..." 等绝对 URI 形式) + if (target[0] != '/') return false; + + // 拒绝空字节(防止截断绕过) + if (target.find('\0') != std::string::npos) return false; + + // 拒绝路径遍历:明文 ".." + if (target.find("..") != std::string::npos) return false; + + // 拒绝路径遍历:URL 编码形式(%2e / %2E,含 %2f / %5c 的编码变体) + std::string lower_target; + lower_target.reserve(target.size()); + for (char c : target) { + lower_target.push_back(static_cast(std::tolower(static_cast(c)))); + } + if (lower_target.find("%2e") != std::string::npos) return false; + if (lower_target.find("%2f") != std::string::npos) return false; + if (lower_target.find("%5c") != std::string::npos) return false; + + return true; } diff --git a/waf/internal/proxy/proxy.go b/waf/internal/proxy/proxy.go index 66e549d..30f5cff 100644 --- a/waf/internal/proxy/proxy.go +++ b/waf/internal/proxy/proxy.go @@ -35,7 +35,13 @@ func New(upstream string, waf *engine.Engine) (*Proxy, error) { origDirector := rp.Director rp.Director = func(req *http.Request) { origDirector(req) - req.Header.Set("X-Forwarded-For", req.RemoteAddr) + // RemoteAddr 形如 "1.2.3.4:54321",剥离端口只保留 IP, + // 否则 gateway RateLimiter 会把同 IP 不同端口的连接当成不同客户端 + clientIP := req.RemoteAddr + if idx := strings.LastIndexByte(req.RemoteAddr, ':'); idx != -1 { + clientIP = req.RemoteAddr[:idx] + } + req.Header.Set("X-Forwarded-For", clientIP) req.Header.Set("X-Forwarded-Proto", "https") } // 透传错误,不暴露内部堆栈