Merge pull request '修复bug' (#4) from dev_qing into main

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
qing committed 2026-07-25 11:19:12 +08:00
commit 1639c6579c
7 files changed
+197 -34

No files matched your search

+6 -6
View File
@@ -20,8 +20,8 @@ SYSTEM_PROMPT = """You are a strict content moderation AI. Your task is to revie
## Output format: ## Output format:
Return ONLY a valid JSON object, no other text. Return ONLY a valid JSON object, no other text.
For violation: {{"invalid": "true"}} For violation: {{"invalid": true}}
For safe content: {{"invalid": "false"}} For safe content: {{"invalid": false}}
## Reference examples: ## Reference examples:
{examples} {examples}
@@ -42,7 +42,7 @@ class ReviewAgent:
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"), base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
) )
self.rag = RAGStore() 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.temperature = float(os.getenv("DEEPSEEK_TEMPERATURE", "0.0"))
self.max_tokens = int(os.getenv("DEEPSEEK_MAX_TOKENS", "128")) self.max_tokens = int(os.getenv("DEEPSEEK_MAX_TOKENS", "128"))
@@ -68,7 +68,7 @@ class ReviewAgent:
) )
raw = response.choices[0].message.content.strip() raw = response.choices[0].message.content.strip()
except Exception as e: except Exception as e:
return {"invalid": "false", "error": "api_error", "detail": str(e)} return {"invalid": "true", "error": "api_error", "detail": str(e)}
try: try:
result = json.loads(raw) result = json.loads(raw)
@@ -77,8 +77,8 @@ class ReviewAgent:
if '"invalid"' in raw_lower and 'true' in raw_lower: if '"invalid"' in raw_lower and 'true' in raw_lower:
result = {"invalid": "true"} result = {"invalid": "true"}
elif '"invalid"' in raw_lower and 'false' in raw_lower: elif '"invalid"' in raw_lower and 'false' in raw_lower:
result = {"invalid": "false"} result = {"invalid": "true"}
else: else:
result = {"invalid": "false", "error": "parse_failed", "raw": raw} result = {"invalid": "true", "error": "parse_failed", "raw": raw}
return result return result
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
cd "$(dirname "$0")" || exit cd "$(dirname "$0")" || exit
python3 captcha_service.py exec python3 captcha_service.py
+31 -3
View File
@@ -100,7 +100,11 @@ def review_content(content: str, timeout: int = 10) -> Dict[str, Any]:
with urllib.request.urlopen(req, timeout=timeout) as resp: with urllib.request.urlopen(req, timeout=timeout) as resp:
resp_data = resp.read().decode("utf-8") resp_data = resp.read().decode("utf-8")
data = json.loads(resp_data) 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: except urllib.error.HTTPError as e:
# 服务端返回 4xx/5xx 错误时,尝试解析错误正文 # 服务端返回 4xx/5xx 错误时,尝试解析错误正文
error_body = e.read().decode("utf-8") error_body = e.read().decode("utf-8")
@@ -197,8 +201,9 @@ def add_note():
# 内容审查 # 内容审查
review = review_content(title + '\n\n' + content, 10) review = review_content(title + '\n' + content, 10)
if not review: if True != review:
#return jsonify({'error': 'review failed', 'data': review}), 400
return jsonify({'error': 'review failed'}), 400 return jsonify({'error': 'review failed'}), 400
insert_payload = { insert_payload = {
@@ -351,6 +356,29 @@ def update_note():
return jsonify({"error": "Invalid param"}), 400 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(): #with app.app_context():
# note = Note.query.get(id) # note = Note.query.get(id)
+14 -5
View File
@@ -161,15 +161,23 @@ protected:
virtual std::string build_message() const { virtual std::string build_message() const {
std::string result; std::string result;
// 错误码 // 错误码
if (m_code != 0) { if (m_code != 0) {
result += "[Error " + std::to_string(m_code) + "] "; result += "[Error " + std::to_string(m_code) + "] ";
} }
// 错误消息 // 错误消息
result += m_message; result += m_message;
/* ===== 以下「位置信息」与「嵌套异常」暂时以注释方式保留 =====
* 原因:黑盒检测发现 what() 的返回值会透传到 HTTP 响应体,
* 导致泄露源码文件路径、行号、C++ 模板类型签名(函数名),
* 便于攻击者定位代码、确认技术栈、构造针对性 payload(P0-3)。
* 排障:完整信息仍由各 handler 通过 std::cerr 输出到服务端日志。
* 恢复:如需调试可临时取消下方注释;生产环境必须保持注释状态,
* 并确保 what() 不直接进入客户端响应。
* ====================================================================
// 位置信息 // 位置信息
if (!m_file.empty()) { if (!m_file.empty()) {
result += " (at " + m_file; result += " (at " + m_file;
@@ -181,12 +189,13 @@ protected:
} }
result += ")"; result += ")";
} }
// 嵌套异常 // 嵌套异常
if (m_nested) { if (m_nested) {
result += "\n Caused by: " + std::string(m_nested->what()); result += "\n Caused by: " + std::string(m_nested->what());
} }
* ==================================================================== */
return result; return result;
} }
+2 -1
View File
@@ -1,3 +1,4 @@
#pragma once
#include <chrono> #include <chrono>
#include <string> #include <string>
#include <format> #include <format>
@@ -13,7 +14,7 @@ public:
auto now = system_clock::now(); auto now = system_clock::now();
auto zone = current_zone(); auto zone = current_zone();
zoned_time zt{zone, now}; 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) // 解析时间字符串为 system_clock::time_point(本地时间 → UTC)
+136 -17
View File
@@ -5,6 +5,7 @@
#include <us/SHA256.hpp> #include <us/SHA256.hpp>
#include <us/Sqlite3.hpp> #include <us/Sqlite3.hpp>
#include <iostream> #include <iostream>
#include <cctype>
#include <typeinfo> #include <typeinfo>
#include <memory> #include <memory>
#include <csignal> #include <csignal>
@@ -151,9 +152,12 @@ public:
bool has_upper = false, has_lower = false, has_digit = false, has_special = false; bool has_upper = false, has_lower = false, has_digit = false, has_special = false;
for (char c : password) { for (char c : password) {
if (std::isupper(c)) has_upper = true; // std::isupper 等函数参数为 int,char 为有符号且非 ASCII 时是 UB
else if (std::islower(c)) has_lower = true; // 必须 static_cast<unsigned char> 后再传入(P1-2)
else if (std::isdigit(c)) has_digit = true; unsigned char uc = static_cast<unsigned char>(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; else has_special = true;
} }
@@ -277,6 +281,9 @@ http::response<http::string_body> handleCaptcha(
/* headers can't processed */ /* headers can't processed */
bool is_special_header(const std::string &header); bool is_special_header(const std::string &header);
/* 请求路径安全性校验(防路径遍历/协议注入/空字节) */
bool is_safe_target(const std::string& target);
/* configure items */ /* configure items */
static std::string _ADDR; static std::string _ADDR;
static int _PORT; static int _PORT;
@@ -304,8 +311,10 @@ static std::mutex _CAPTCHA_MTX;
std::string generate_captcha_text() { std::string generate_captcha_text() {
static const char chars[] = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; static const char chars[] = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
static std::mt19937 rng(std::random_device{}()); // thread_local:每个线程独立 rng 实例,避免多线程并发调用 dist(rng) 时的数据竞争
static std::uniform_int_distribution<int> dist(0, sizeof(chars) - 2); // (static 变量初始化本身线程安全,但 mt19937::operator() 修改内部状态非线程安全)
thread_local std::mt19937 rng(std::random_device{}());
thread_local std::uniform_int_distribution<int> dist(0, sizeof(chars) - 2);
std::string text; std::string text;
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
@@ -497,7 +506,28 @@ void registerRoutes(HttpServer &server) {
server.options("*", [](const auto& req, const auto& params) { server.options("*", [](const auto& req, const auto& params) {
pmc::net::http::response<pmc::net::http::string_body> res; pmc::net::http::response<pmc::net::http::string_body> res;
res.result(pmc::net::http::status::ok); 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<std::string> 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_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_allow_headers, "Content-Type, Authorization");
res.set(pmc::net::http::field::access_control_max_age, "86400"); res.set(pmc::net::http::field::access_control_max_age, "86400");
@@ -523,11 +553,17 @@ catch (const std::exception& e) {
http::response<http::string_body> res; http::response<http::string_body> res;
res.version(req.version()); res.version(req.version());
const auto& ti = typeid(e); 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 = { json::value jv_r = {
{"success", false}, {"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.result(http::status::internal_server_error);
res.body() = json::serialize(jv_r); res.body() = json::serialize(jv_r);
res.prepare_payload(); res.prepare_payload();
@@ -695,9 +731,14 @@ http::response<http::string_body> handleLogin(
catch (const HttpException& e) { catch (const HttpException& e) {
// 详细错误信息(e.what() 含源码路径/行号/C++ 模板类型)暂时以注释方式保留
// 原因:黑盒检测发现透传到客户端会泄露内部实现,便于攻击者定位代码(P0-3)
// 排障:完整信息仍输出到服务端 stderr
std::cerr << "[handleLogin HttpException] " << e.what() << std::endl;
json::value jv_r = { json::value jv_r = {
{"success", false}, {"success", false},
{"message", e.what()} /* {"message", e.what()}, */ // 泄露源码信息,已禁用
{"message", "Invalid Username or Password"}
}; };
res.set(http::field::content_type, "application/json"); res.set(http::field::content_type, "application/json");
res.result(e.status()); res.result(e.status());
@@ -717,6 +758,10 @@ http::response<http::string_body> handleRegister(
// IP rate limit check // IP rate limit check
std::string client_ip = "direct"; 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)) { if (!_REGISTER_RATE.check_and_record(client_ip, 3, 600)) {
json::value jv_r = { json::value jv_r = {
{"success", false}, {"success", false},
@@ -793,9 +838,12 @@ http::response<http::string_body> handleRegister(
} }
catch (const HttpException& e) { catch (const HttpException& e) {
// 详细错误信息(e.what() 含源码路径/行号/C++ 模板类型)暂时以注释方式保留(同 handleLogin)
std::cerr << "[handleRegister HttpException] " << e.what() << std::endl;
json::value jv_r = { json::value jv_r = {
{"success", false}, {"success", false},
{"message", e.what()} /* {"message", e.what()}, */ // 泄露源码信息,已禁用
{"message", "Registration failed"}
}; };
res.set(http::field::content_type, "application/json"); res.set(http::field::content_type, "application/json");
res.result(e.status()); res.result(e.status());
@@ -804,9 +852,19 @@ http::response<http::string_body> handleRegister(
catch (const std::exception& e) { catch (const std::exception& e) {
const auto& ti = typeid(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.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(); res.prepare_payload();
@@ -845,9 +903,17 @@ http::response<http::string_body> handleProxy(
flag = true; 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 = t_url;
url += "/"; url += "/";
url += req.target(); url += target_path;
it = req.base().find("Authorization"); it = req.base().find("Authorization");
if (it == req.base().end() && !flag) { if (it == req.base().end() && !flag) {
@@ -915,6 +981,18 @@ http::response<http::string_body> handleProxy(
client.set_header(h); 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") if (method == "get")
client.get_stream_sync(url, callback); client.get_stream_sync(url, callback);
@@ -934,12 +1012,17 @@ http::response<http::string_body> handleProxy(
catch (const std::exception& e) { catch (const std::exception& e) {
const auto& ti = typeid(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); res.result(http::status::internal_server_error);
json::value jv_r = { json::value jv_r = {
{"success", false}, {"success", false},
{"err_name", ti.name()}, /* {"err_name", ti.name()}, // 泄露 C++ 类型信息,已禁用 */
{"err_message", e.what()} /* {"err_message", e.what()} // 泄露内部细节,已禁用 */
{"message", "Internal Server Error"}
}; };
res.body() = json::serialize(jv_r); res.body() = json::serialize(jv_r);
} }
@@ -950,6 +1033,42 @@ http::response<http::string_body> handleProxy(
bool is_special_header(const std::string &header) { 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" 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<char>(std::tolower(static_cast<unsigned char>(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;
} }
+7 -1
View File
@@ -35,7 +35,13 @@ func New(upstream string, waf *engine.Engine) (*Proxy, error) {
origDirector := rp.Director origDirector := rp.Director
rp.Director = func(req *http.Request) { rp.Director = func(req *http.Request) {
origDirector(req) 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") req.Header.Set("X-Forwarded-Proto", "https")
} }
// 透传错误,不暴露内部堆栈 // 透传错误,不暴露内部堆栈