Merge pull request '节点所有权问题' (#6) from dev_qing into main

Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
qing committed 2026-07-25 14:30:01 +08:00
commit dff9b42bea
8 files changed
+253 -45

No files matched your search

+4
View File
@@ -13,6 +13,10 @@ CREATE TABLE IF NOT EXISTS Users (
CREATE TABLE IF NOT EXISTS Notes ( CREATE TABLE IF NOT EXISTS Notes (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
pid INTEGER NOT NULL, pid INTEGER NOT NULL,
owner VARCHAR(128) DEFAULT 'Unknow',
title VARCHAR(128) NOT NULL, title VARCHAR(128) NOT NULL,
content TEXT NOT NULL content TEXT NOT NULL
); );
-- 旧版增量
-- ALTER TABLE Notes ADD COLUMN owner VARCHAR(128) DEFAULT 'Unknow';
+65 -32
View File
@@ -22,6 +22,7 @@ from flask_cors import CORS
from waitress import serve from waitress import serve
import os import os
import json import json
import base64
import urllib.request import urllib.request
import urllib.error import urllib.error
from typing import Dict, Any from typing import Dict, Any
@@ -157,7 +158,18 @@ CORS(app)
# "pid": 0, # "pid": 0,
# "title": "test", # "title": "test",
# "content": "hello" # "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']) @app.route('/add_note', methods=['POST'])
def add_note(): def add_note():
@@ -174,14 +186,14 @@ def add_note():
return jsonify({"error": "Pid, title and content are required"}), 400 return jsonify({"error": "Pid, title and content are required"}), 400
#new_note = Note(pid=int(pid), title=title, content=content) info = None
auth: str = request.headers.get('Authorization', '')
#with app.app_context(): if auth.startswith('Bearer '):
token: str = auth[7:]
# db.session.add(new_note) info = decode_token(token)
# db.session.commit()
if info is None:
return jsonify({"error": "Warning: no Authorization"}), 401
if pid != 0: if pid != 0:
@@ -211,6 +223,7 @@ def add_note():
'table': 'Notes', 'table': 'Notes',
'values': { 'values': {
"pid": pid, "pid": pid,
'owner': info['user'],
"title": title, "title": title,
'content': content 'content': content
} }
@@ -247,17 +260,6 @@ def get_notes():
notes = call_db_service(DB_SERVER_URL, select_payload) 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'] result = notes['data']
return jsonify(result), 200 return jsonify(result), 200
@@ -276,6 +278,15 @@ def delete_note():
return jsonify({"error": "Note ID is required"}), 400 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(): #with app.app_context():
# notes = Note.query.filter(Note.pid == int(note_id)) # notes = Note.query.filter(Note.pid == int(note_id))
@@ -295,6 +306,7 @@ def delete_note():
# return jsonify({"error": "Note not found"}), 404 # return jsonify({"error": "Note not found"}), 404
# 检查是否有子节点
select_payload = { select_payload = {
'action': 'select', 'action': 'select',
'table': 'Notes', 'table': 'Notes',
@@ -310,6 +322,20 @@ def delete_note():
return jsonify({'error': 'this node has a child '}), 400 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 = { delete_payload = {
@@ -355,23 +381,30 @@ def update_note():
return jsonify({"error": "Invalid param"}), 400 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', select_payload = {
'where': { 'action': 'select',
'id': pid 'table': 'Notes',
} 'where': {
'id': id
} }
}
notes = call_db_service(DB_SERVER_URL, select_payload) data = call_db_service(DB_SERVER_URL, select_payload)
notes = data['data']
# 不存在的父节点 if not len(notes) == 1 or not notes[0]['owner'] == info['user']:
if len( notes['data']) == 0: return jsonify({"error": 'It\'s not your note'}), 403
return jsonify({'error': 'the new node need a parent'}), 400
# 内容审查 # 内容审查
review = review_content(title + '\n' + content, 10) review = review_content(title + '\n' + content, 10)
+11
View File
@@ -125,6 +125,15 @@ default = http://127.0.0.1:9200
closed = http://127.0.0.1:8005;http://127.0.0.1:8007; 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; 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] [Database]
path = sqlite3.db path = sqlite3.db
``` ```
@@ -132,6 +141,8 @@ path = sqlite3.db
- `Proxy.default`:未带 `Target-Url` header 时的默认转发地址(不需 Token,主要服务前端静态资源)。 - `Proxy.default`:未带 `Target-Url` header 时的默认转发地址(不需 Token,主要服务前端静态资源)。
- `Proxy.opened`:允许外部带 Token 访问的内部服务白名单(分号分隔)。 - `Proxy.opened`:允许外部带 Token 访问的内部服务白名单(分号分隔)。
- `Proxy.closed`:仅记录用途,实际靠"不在 opened 中即拒绝"实现隔离。 - `Proxy.closed`:仅记录用途,实际靠"不在 opened 中即拒绝"实现隔离。
- `Token.keep_time`:登录签发 Token 的有效期(秒),缺省 3600。
- `CORS.allowed_origins`:OPTIONS 预检允许跨域的 Origin 白名单(分号分隔),缺省为空(不回填 ACAO)。
## 部署模式 ## 部署模式
+9
View File
@@ -11,5 +11,14 @@ default = http://127.0.0.1:9200
closed = http://127.0.0.1:8005;http://127.0.0.1:8007; 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; 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] [Database]
path = sqlite3.db path = sqlite3.db
+12 -13
View File
@@ -37,7 +37,11 @@ std::vector<std::string> split(const std::string& s, char delim) {
std::istringstream stream(s); std::istringstream stream(s);
std::string token; std::string token;
while (std::getline(stream, token, delim)) { while (std::getline(stream, token, delim)) {
tokens.push_back(token); // 跳过空串:避免 "a;b;" 末尾分号产生 "" 元素
// 防御白名单匹配场景下空串被误判为命中
if (!token.empty()) {
tokens.push_back(token);
}
} }
return tokens; return tokens;
} }
@@ -291,6 +295,8 @@ static bool _TLS = false;
static std::string _DEFT_URL; static std::string _DEFT_URL;
static std::vector<std::string> _OPEN_URL; static std::vector<std::string> _OPEN_URL;
static std::string _DB_PATH; static std::string _DB_PATH;
static int _TOKEN_KEEP_TIME = 3600; // Token 有效期(秒),可由 [Token] keep_time 配置
static std::vector<std::string> _CORS_ORIGINS; // CORS 允许的 Origin 白名单,可由 [CORS] allowed_origins 配置
/* instance items */ /* instance items */
static std::unique_ptr<HttpServer> _SERVE; static std::unique_ptr<HttpServer> _SERVE;
@@ -399,6 +405,8 @@ int main(int argc, char **argv) {
_DEFT_URL = cf.get<std::string>("Proxy.default", ""); _DEFT_URL = cf.get<std::string>("Proxy.default", "");
_OPEN_URL = split(cf.get<std::string>("Proxy.opened", ""), ';'); _OPEN_URL = split(cf.get<std::string>("Proxy.opened", ""), ';');
_DB_PATH = cf.get<std::string>("Database.path", ""); _DB_PATH = cf.get<std::string>("Database.path", "");
_TOKEN_KEEP_TIME = cf.get<int>("Token.keep_time", 3600);
_CORS_ORIGINS = split(cf.get<std::string>("CORS.allowed_origins", ""), ';');
} }
catch (ConfIni::IniConfigureFileParseException &e) { catch (ConfIni::IniConfigureFileParseException &e) {
std::cerr << "ERROR: " << e.what() << std::endl; std::cerr << "ERROR: " << e.what() << std::endl;
@@ -507,19 +515,12 @@ void registerRoutes(HttpServer &server) {
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);
// CORS 白名单(P1-4):仅允许官方域名,防止任意网站跨域调用 // CORS 白名单:从 [CORS] allowed_origins 加载,缺省为空(不回填 ACAO)
// 原配置 Access-Control-Allow-Origin: * 过宽,XSS 拿到 token 后可跨域读取数据
// 动态回填 Origin:仅当请求 Origin 命中白名单时才返回该值,否则不返回 ACAO 头 // 动态回填 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"); auto origin_it = req.base().find("Origin");
if (origin_it != req.base().end()) { if (origin_it != req.base().end()) {
std::string origin = std::string(origin_it->value()); std::string origin = std::string(origin_it->value());
for (const auto& allowed : allowed_origins) { for (const auto& allowed : _CORS_ORIGINS) {
if (origin == allowed) { if (origin == allowed) {
res.set(pmc::net::http::field::access_control_allow_origin, origin); res.set(pmc::net::http::field::access_control_allow_origin, origin);
res.set(pmc::net::http::field::vary, "Origin"); res.set(pmc::net::http::field::vary, "Origin");
@@ -709,12 +710,10 @@ http::response<http::string_body> handleLogin(
auto _PRIV = MyRSA::Private_Key::from_pem(_KEYGEN->get_private_key_pem()); auto _PRIV = MyRSA::Private_Key::from_pem(_KEYGEN->get_private_key_pem());
int keep_time = 3600;
json::value jv_t = { json::value jv_t = {
{"user", username}, {"user", username},
{"created_at", LocalTime::getCurrent()}, {"created_at", LocalTime::getCurrent()},
{"keep_time", keep_time} {"keep_time", _TOKEN_KEEP_TIME}
}; };
std::string data = json::serialize(jv_t); std::string data = json::serialize(jv_t);
std::string token = Base64::base64_encode(data); std::string token = Base64::base64_encode(data);
View File
Whitespace-only changes.
+144
View File
@@ -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)"
+8
View File
@@ -4,8 +4,15 @@
# - 修改后执行 `kill -HUP <pid>` 热加载 # - 修改后执行 `kill -HUP <pid>` 热加载
# - 校验语法: `waf -test rules/local/xxx.conf` # - 校验语法: `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 分段约定: # 规则 ID 分段约定:
# 100000-199999 本地自定义规则 # 100000-199999 本地自定义规则
# 200000-200006 请求体处理器初始化(crs/coraza.conf)
# 900000-999999 OWASP CRS(由 crs/ 提供,不手改) # 900000-999999 OWASP CRS(由 crs/ 提供,不手改)
# #
# 注意:本文件不用 Include 指令(coraza 的 Include 路径相对于 cwd 解析, # 注意:本文件不用 Include 指令(coraza 的 Include 路径相对于 cwd 解析,
@@ -16,6 +23,7 @@ SecRuleEngine On
SecRequestBodyAccess On SecRequestBodyAccess On
SecResponseBodyAccess On SecResponseBodyAccess On
SecDefaultAction "phase:1,pass,log,auditlog" SecDefaultAction "phase:1,pass,log,auditlog"
SecDefaultAction "phase:2,pass,log,auditlog"
# 请求体限制 10MB # 请求体限制 10MB
SecRequestBodyLimit 10485760 SecRequestBodyLimit 10485760