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:
commit
1639c6579c
7 files changed
+197
-34
No files matched your search
@@ -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
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
python3 captcha_service.py
|
||||
exec python3 captcha_service.py
|
||||
+31
-3
@@ -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)
|
||||
|
||||
+14
-5
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <format>
|
||||
@@ -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)
|
||||
|
||||
+136
-17
@@ -5,6 +5,7 @@
|
||||
#include <us/SHA256.hpp>
|
||||
#include <us/Sqlite3.hpp>
|
||||
#include <iostream>
|
||||
#include <cctype>
|
||||
#include <typeinfo>
|
||||
#include <memory>
|
||||
#include <csignal>
|
||||
@@ -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<unsigned char> 后再传入(P1-2)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -277,6 +281,9 @@ http::response<http::string_body> 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<int> 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<int> 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<pmc::net::http::string_body> 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<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_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<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<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;
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
// 透传错误,不暴露内部堆栈
|
||||
|
||||
Reference in new issue
Block a user