From 15a4bac484c8d4bc6f385fdc004956074f407e3e Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sat, 25 Jul 2026 11:22:51 +0800 Subject: [PATCH 1/5] =?UTF-8?q?waf=E8=A7=84=E5=88=99=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=8B=89=E5=8F=96=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- waf/_fetch_crs.sh | 144 +++++++++++++++++++++++++++++++++++++++++++++ waf/rules/waf.conf | 8 +++ 2 files changed, 152 insertions(+) create mode 100755 waf/_fetch_crs.sh diff --git a/waf/_fetch_crs.sh b/waf/_fetch_crs.sh new file mode 100755 index 0000000..32f87f3 --- /dev/null +++ b/waf/_fetch_crs.sh @@ -0,0 +1,144 @@ +#!/bin/bash +# 拉取 OWASP CRS 规则(通过 Go module proxy,不依赖 GitHub 直连) +# +# 用法: +# bash _fetch_crs.sh # 拉取最新版 +# bash _fetch_crs.sh v4.25.0 # 拉取指定版本 +# +# 规则来源:github.com/corazawaf/coraza-coreruleset/v4(Apache-2.0) +# 规则文件放置在 rules/crs/rules/,入口配置 rules/crs/coraza.conf +# 整个 rules/crs/ 目录已在 .gitignore 中,不入库。 +set -euo pipefail +cd "$(dirname "$0")" || exit + +VERSION="${1:-latest}" +CRS_DIR="rules/crs" +RULES_DIR="$CRS_DIR/rules" + +echo "==== 拉取 OWASP CRS (${VERSION}) ====" + +# 1. 通过 Go module proxy 下载 coraza-coreruleset +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT +WORKDIR="$TMPDIR/crs-fetch" +mkdir -p "$WORKDIR" + +cd "$WORKDIR" +cat > go.mod << 'EOF' +module crs-fetch +go 1.22.0 +EOF + +echo "[1/4] 下载 coraza-coreruleset (${VERSION}) ..." +if [ "$VERSION" = "latest" ]; then + GOPROXY=https://goproxy.cn,direct go mod download github.com/corazawaf/coraza-coreruleset/v4@latest 2>&1 +else + GOPROXY=https://goproxy.cn,direct go mod download "github.com/corazawaf/coraza-coreruleset/v4@${VERSION}" 2>&1 +fi + +# 2. 定位模块缓存 +GOMODCACHE=$(go env GOMODCACHE) +# 找到实际下载的版本目录(可能是 v4@v4.25.0 等形式) +MOD_DIR=$(ls -d "$GOMODCACHE"/github.com/corazawaf/coraza-coreruleset/v4@* 2>/dev/null | tail -1) +if [ -z "$MOD_DIR" ]; then + echo "[ERROR] 未在模块缓存中找到 coraza-coreruleset" >&2 + exit 1 +fi +ACTUAL_VER=$(basename "$MOD_DIR" | sed 's/v4@//') +echo " 实际版本: ${ACTUAL_VER}" + +SRC_RULES="$MOD_DIR/rules/@owasp_crs" +if [ ! -d "$SRC_RULES" ]; then + echo "[ERROR] 规则目录不存在: $SRC_RULES" >&2 + exit 1 +fi + +cd - > /dev/null + +# 3. 复制规则文件 +echo "[2/4] 复制规则文件到 ${RULES_DIR}/ ..." +mkdir -p "$RULES_DIR" +cp "$SRC_RULES"/*.conf "$SRC_RULES"/*.data "$RULES_DIR/" +CONF_COUNT=$(ls "$RULES_DIR"/*.conf 2>/dev/null | wc -l | tr -d ' ') +DATA_COUNT=$(ls "$RULES_DIR"/*.data 2>/dev/null | wc -l | tr -d ' ') +echo " ${CONF_COUNT} 个规则文件, ${DATA_COUNT} 个数据文件" + +# 4. 复制 LICENSE +echo "[3/4] 复制 LICENSE ..." +cp "$MOD_DIR/rules/LICENSE" "$CRS_DIR/LICENSE" 2>/dev/null || true + +# 5. 生成入口配置 crs/coraza.conf +echo "[4/4] 生成 ${CRS_DIR}/coraza.conf ..." +# 从版本号算出 crs_setup_version:v4.25.0 → 4250 +SETUP_VER=$(echo "$ACTUAL_VER" | sed 's/^v//; s/\.//g; s/\([0-9]*\).*/\1/' | head -c 4) +# 更精确:取 major*1000 + minor*10 + patch +MAJOR=$(echo "$ACTUAL_VER" | sed 's/^v//; s/\..*//') +MINOR=$(echo "$ACTUAL_VER" | sed 's/^v[0-9]*\.//; s/\..*//') +PATCH=$(echo "$ACTUAL_VER" | sed 's/^v[0-9]*\.[0-9]*\.//; s/[^0-9].*//') +SETUP_VER="${MAJOR}${MINOR}${PATCH}" + +cat > "$CRS_DIR/coraza.conf" << CONFEOF +# crs/coraza.conf —— OWASP CRS 入口配置(由 _fetch_crs.sh 自动生成) +# +# 加载顺序(由 engine.Reload 控制): +# 1. rules/waf.conf 基础配置(SecRuleEngine On 等) +# 2. rules/local/*.conf 本地自定义规则 +# 3. rules/crs/coraza.conf ← 本文件(CRS 初始化) +# 4. rules/crs/rules/*.conf CRS 规则文件(按文件名字母序) +# +# 本文件职责: +# - 设置 CRS 必需的事务变量(crs_setup_version) +# - 初始化 XML/JSON 请求体处理器 +# +# 不在此文件设置的项(由 waf.conf 统一管理): +# - SecRuleEngine / SecRequestBodyAccess / SecResponseBodyAccess +# - SecRequestBodyLimit +# - SecDefaultAction(coraza 不允许同 phase 重复定义) + +# ===== 请求体处理器初始化 ===== +SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \\ + "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + +SecRule REQUEST_HEADERS:Content-Type "^application/json" \\ + "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + +SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \\ + "id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + +# ===== CRS 必需变量 ===== +# 规则 901001 检查此变量,未设置则返回 500 +SecAction \\ + "id:900990,\\ + phase:1,\\ + pass,\\ + nolog,\\ + setvar:tx.crs_setup_version=${SETUP_VER}" + +# ===== 可选调优(取消注释以启用) ===== +# 异常评分阈值(默认 inbound=5, outbound=4) +# SecAction \\ +# "id:900110,\\ +# phase:1,\\ +# pass,\\ +# nolog,\\ +# setvar:tx.inbound_anomaly_score_threshold=5,\\ +# setvar:tx.outbound_anomaly_score_threshold=4" + +# 偏执等级(默认 1;等级越高规则越严、误报越多) +# SecAction \\ +# "id:900000,\\ +# phase:1,\\ +# pass,\\ +# nolog,\\ +# setvar:tx.blocking_paranoia_level=1" +CONFEOF + +echo "" +echo "==== 完成 ====" +echo " 版本: OWASP CRS ${ACTUAL_VER}" +echo " 规则: ${RULES_DIR}/ (${CONF_COUNT} conf + ${DATA_COUNT} data)" +echo " 入口: ${CRS_DIR}/coraza.conf" +echo " 许可: Apache-2.0 (${CRS_DIR}/LICENSE)" +echo "" +echo "验证: ./waf.bin -c config.yaml -list | head" +echo "热加载: kill -HUP \$(pidof waf.bin)" diff --git a/waf/rules/waf.conf b/waf/rules/waf.conf index 1159b0c..ac74d68 100644 --- a/waf/rules/waf.conf +++ b/waf/rules/waf.conf @@ -4,8 +4,15 @@ # - 修改后执行 `kill -HUP ` 热加载 # - 校验语法: `waf -test rules/local/xxx.conf` # +# 加载顺序(engine.Reload): +# 1. waf.conf ← 本文件(引擎基础配置) +# 2. local/*.conf 本地自定义规则(按文件名字母序) +# 3. crs/coraza.conf OWASP CRS 入口(若存在) +# 4. crs/rules/*.conf OWASP CRS 规则(按文件名字母序) +# # 规则 ID 分段约定: # 100000-199999 本地自定义规则 +# 200000-200006 请求体处理器初始化(crs/coraza.conf) # 900000-999999 OWASP CRS(由 crs/ 提供,不手改) # # 注意:本文件不用 Include 指令(coraza 的 Include 路径相对于 cwd 解析, @@ -16,6 +23,7 @@ SecRuleEngine On SecRequestBodyAccess On SecResponseBodyAccess On SecDefaultAction "phase:1,pass,log,auditlog" +SecDefaultAction "phase:2,pass,log,auditlog" # 请求体限制 10MB SecRequestBodyLimit 10485760 From 46b8f750ae86aa5686a126b6b9c81ddac5bea5e0 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sat, 25 Jul 2026 13:07:18 +0800 Subject: [PATCH 2/5] fixbug --- gateway/README.md | 11 +++++++++++ gateway/example.ini | 9 +++++++++ gateway/src/main.cpp | 25 ++++++++++++------------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/gateway/README.md b/gateway/README.md index 98a943e..6544096 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -125,6 +125,15 @@ default = http://127.0.0.1:9200 closed = http://127.0.0.1:8005;http://127.0.0.1:8007; opened = http://127.0.0.1:9200;http://127.0.0.1:8006; +[Token] +# Token 有效期(秒),默认 3600 = 1 小时 +keep_time = 3600 + +[CORS] +# 允许跨域的 Origin 白名单(分号分隔) +# 仅命中的 Origin 才会回填 Access-Control-Allow-Origin;缺省为空(不回填) +allowed_origins = https://qsont.me;https://www.qsont.me;http://127.0.0.1:8443;http://localhost:8443 + [Database] path = sqlite3.db ``` @@ -132,6 +141,8 @@ path = sqlite3.db - `Proxy.default`:未带 `Target-Url` header 时的默认转发地址(不需 Token,主要服务前端静态资源)。 - `Proxy.opened`:允许外部带 Token 访问的内部服务白名单(分号分隔)。 - `Proxy.closed`:仅记录用途,实际靠"不在 opened 中即拒绝"实现隔离。 +- `Token.keep_time`:登录签发 Token 的有效期(秒),缺省 3600。 +- `CORS.allowed_origins`:OPTIONS 预检允许跨域的 Origin 白名单(分号分隔),缺省为空(不回填 ACAO)。 ## 部署模式 diff --git a/gateway/example.ini b/gateway/example.ini index 617b5f6..f6fbd24 100644 --- a/gateway/example.ini +++ b/gateway/example.ini @@ -11,5 +11,14 @@ default = http://127.0.0.1:9200 closed = http://127.0.0.1:8005;http://127.0.0.1:8007; opened = http://127.0.0.1:9200;http://127.0.0.1:8006; +[Token] +# Token 有效期(秒),默认 3600 = 1 小时 +keep_time = 3600 + +[CORS] +# 允许跨域的 Origin 白名单(分号分隔) +# 仅命中的 Origin 才会回填 Access-Control-Allow-Origin;缺省为空(不回填) +allowed_origins = https://qsont.me;https://www.qsont.me;http://127.0.0.1:8443;http://localhost:8443 + [Database] path = sqlite3.db diff --git a/gateway/src/main.cpp b/gateway/src/main.cpp index 9a0ae2d..ac2d552 100644 --- a/gateway/src/main.cpp +++ b/gateway/src/main.cpp @@ -37,7 +37,11 @@ std::vector split(const std::string& s, char delim) { std::istringstream stream(s); std::string token; while (std::getline(stream, token, delim)) { - tokens.push_back(token); + // 跳过空串:避免 "a;b;" 末尾分号产生 "" 元素 + // 防御白名单匹配场景下空串被误判为命中 + if (!token.empty()) { + tokens.push_back(token); + } } return tokens; } @@ -291,6 +295,8 @@ static bool _TLS = false; static std::string _DEFT_URL; static std::vector _OPEN_URL; static std::string _DB_PATH; +static int _TOKEN_KEEP_TIME = 3600; // Token 有效期(秒),可由 [Token] keep_time 配置 +static std::vector _CORS_ORIGINS; // CORS 允许的 Origin 白名单,可由 [CORS] allowed_origins 配置 /* instance items */ static std::unique_ptr _SERVE; @@ -399,6 +405,8 @@ int main(int argc, char **argv) { _DEFT_URL = cf.get("Proxy.default", ""); _OPEN_URL = split(cf.get("Proxy.opened", ""), ';'); _DB_PATH = cf.get("Database.path", ""); + _TOKEN_KEEP_TIME = cf.get("Token.keep_time", 3600); + _CORS_ORIGINS = split(cf.get("CORS.allowed_origins", ""), ';'); } catch (ConfIni::IniConfigureFileParseException &e) { std::cerr << "ERROR: " << e.what() << std::endl; @@ -507,19 +515,12 @@ void registerRoutes(HttpServer &server) { pmc::net::http::response res; res.result(pmc::net::http::status::ok); - // CORS 白名单(P1-4):仅允许官方域名,防止任意网站跨域调用 - // 原配置 Access-Control-Allow-Origin: * 过宽,XSS 拿到 token 后可跨域读取数据 + // CORS 白名单:从 [CORS] allowed_origins 加载,缺省为空(不回填 ACAO) // 动态回填 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) { + for (const auto& allowed : _CORS_ORIGINS) { if (origin == allowed) { res.set(pmc::net::http::field::access_control_allow_origin, origin); res.set(pmc::net::http::field::vary, "Origin"); @@ -709,12 +710,10 @@ http::response handleLogin( auto _PRIV = MyRSA::Private_Key::from_pem(_KEYGEN->get_private_key_pem()); - int keep_time = 3600; - json::value jv_t = { {"user", username}, {"created_at", LocalTime::getCurrent()}, - {"keep_time", keep_time} + {"keep_time", _TOKEN_KEEP_TIME} }; std::string data = json::serialize(jv_t); std::string token = Base64::base64_encode(data); From efbe2b36f3d027477b23bd68558e58fe14e98d5c Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sat, 25 Jul 2026 14:20:08 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/create_sqlite3.sql | 4 +++ dnfs/dnfs.py | 63 ++++++++++++++++++++++++++++++------------- smtp/smtp.py | 0 3 files changed, 48 insertions(+), 19 deletions(-) create mode 100644 smtp/smtp.py diff --git a/db/create_sqlite3.sql b/db/create_sqlite3.sql index 3deacb5..28ba662 100644 --- a/db/create_sqlite3.sql +++ b/db/create_sqlite3.sql @@ -13,6 +13,10 @@ CREATE TABLE IF NOT EXISTS Users ( CREATE TABLE IF NOT EXISTS Notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, pid INTEGER NOT NULL, + owner VARCHAR(128) DEFAULT 'Unknow', title VARCHAR(128) NOT NULL, content TEXT NOT NULL ); + +-- 旧版增量 +-- ALTER TABLE Notes ADD COLUMN owner VARCHAR(128) DEFAULT 'Unknow'; diff --git a/dnfs/dnfs.py b/dnfs/dnfs.py index fdae19f..fb89004 100755 --- a/dnfs/dnfs.py +++ b/dnfs/dnfs.py @@ -22,6 +22,7 @@ from flask_cors import CORS from waitress import serve import os import json +import base64 import urllib.request import urllib.error from typing import Dict, Any @@ -157,7 +158,18 @@ CORS(app) # "pid": 0, # "title": "test", # "content": "hello" -#} +#} + +def decode_token(token: str): + idx = token.find('_') + if idx != -1: + raw = token[:idx] + # FIXME: 验证数字签名 + decoded = base64.b64decode(raw) + jj = decoded.decode('utf-8') + return json.loads(jj) + + @app.route('/add_note', methods=['POST']) def add_note(): @@ -174,14 +186,14 @@ def add_note(): return jsonify({"error": "Pid, title and content are required"}), 400 - #new_note = Note(pid=int(pid), title=title, content=content) - - #with app.app_context(): - - # db.session.add(new_note) - - # db.session.commit() + info = None + auth: str = request.headers.get('Authorization', '') + if auth.startswith('Bearer '): + token: str = auth[7:] + info = decode_token(token) + if info is None: + return jsonify({"error": "Warning: no Authorization"}), 401 if pid != 0: @@ -211,6 +223,7 @@ def add_note(): 'table': 'Notes', 'values': { "pid": pid, + 'owner': info['user'], "title": title, 'content': content } @@ -247,17 +260,6 @@ def get_notes(): notes = call_db_service(DB_SERVER_URL, select_payload) - #with app.app_context(): - - # notes = Note.query.filter(Note.pid == int(pid)) - - #result = [{ - # 'id': note['id'], - # 'pid': note['pid'], - # 'title': note['title'], - # 'content':note['content'] - #} for note in notes] - result = notes['data'] return jsonify(result), 200 @@ -276,6 +278,15 @@ def delete_note(): return jsonify({"error": "Note ID is required"}), 400 + info = None + auth: str = request.headers.get('Authorization', '') + if auth.startswith('Bearer '): + token: str = auth[7:] + info = decode_token(token) + + if info is None: + return jsonify({"error": "Warning: no Authorization"}), 401 + #with app.app_context(): # notes = Note.query.filter(Note.pid == int(note_id)) @@ -310,6 +321,20 @@ def delete_note(): return jsonify({'error': 'this node has a child '}), 400 + # 检查所有权 + select_payload = { + 'action': 'select', + 'table': 'Notes', + 'where': { + 'id': note_id + } + } + + data = call_db_service(DB_SERVER_URL, select_payload) + notes = data['data'] + if not info['user'] == 'admin': + if not len(notes) == 1 or not notes[0]['owner'] == info['user']: + return jsonify({"error": 'It\'s not your note'}), 403 delete_payload = { diff --git a/smtp/smtp.py b/smtp/smtp.py new file mode 100644 index 0000000..e69de29 From 3e9ff99eaa7d1f5a6251ef6cadfd3b2550124374 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sat, 25 Jul 2026 14:24:36 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=8F=AA=E8=83=BD=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E8=87=AA=E5=B7=B1=E7=9A=84=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dnfs/dnfs.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/dnfs/dnfs.py b/dnfs/dnfs.py index fb89004..05e8242 100755 --- a/dnfs/dnfs.py +++ b/dnfs/dnfs.py @@ -306,6 +306,7 @@ def delete_note(): # return jsonify({"error": "Note not found"}), 404 + # 检查是否有子节点 select_payload = { 'action': 'select', 'table': 'Notes', @@ -380,23 +381,31 @@ def update_note(): return jsonify({"error": "Invalid param"}), 400 + # 获取用户名 + info = None + auth: str = request.headers.get('Authorization', '') + if auth.startswith('Bearer '): + token: str = auth[7:] + info = decode_token(token) - if pid != 0: + if info is None: + return jsonify({"error": "Warning: no Authorization"}), 401 - select_payload = { - 'action': 'select', - 'table': 'Notes', - 'where': { - 'id': pid - } + + # 检查所有权 + select_payload = { + 'action': 'select', + 'table': 'Notes', + 'where': { + 'id': id } + } - notes = call_db_service(DB_SERVER_URL, select_payload) - - # 不存在的父节点 - if len( notes['data']) == 0: - return jsonify({'error': 'the new node need a parent'}), 400 - + data = call_db_service(DB_SERVER_URL, select_payload) + notes = data['data'] + if not info['user'] == 'admin': + if not len(notes) == 1 or not notes[0]['owner'] == info['user']: + return jsonify({"error": 'It\'s not your note'}), 403 # 内容审查 review = review_content(title + '\n' + content, 10) From 17c22a8cd3635e7b651e8a775d09b3f7f181a0ec Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sat, 25 Jul 2026 14:25:06 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E4=B9=9F?= =?UTF-8?q?=E4=B8=8D=E8=83=BD=E6=9B=B4=E6=96=B0=E5=88=AB=E4=BA=BA=E7=9A=84?= =?UTF-8?q?=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dnfs/dnfs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dnfs/dnfs.py b/dnfs/dnfs.py index 05e8242..2c397cc 100755 --- a/dnfs/dnfs.py +++ b/dnfs/dnfs.py @@ -403,9 +403,8 @@ def update_note(): data = call_db_service(DB_SERVER_URL, select_payload) notes = data['data'] - if not info['user'] == 'admin': - if not len(notes) == 1 or not notes[0]['owner'] == info['user']: - return jsonify({"error": 'It\'s not your note'}), 403 + if not len(notes) == 1 or not notes[0]['owner'] == info['user']: + return jsonify({"error": 'It\'s not your note'}), 403 # 内容审查 review = review_content(title + '\n' + content, 10)