create-dwy 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Project scaffolding and Claude Code config sync CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+ # Claude Code PreToolUse Hook
3
+ # 在 git commit 前检查暂存文件中是否包含敏感数据
4
+
5
+ # 只拦截 git commit 命令
6
+ echo "$TOOL_INPUT" | grep -q 'git commit' || exit 0
7
+
8
+ # 获取暂存文件列表
9
+ STAGED=$(git diff --cached --diff-filter=ACMR --name-only 2>/dev/null)
10
+ [ -z "$STAGED" ] && exit 0
11
+
12
+ # 敏感数据匹配模式
13
+ PATTERNS=(
14
+ # 密码/密钥
15
+ 'password'
16
+ 'passwd'
17
+ 'secret'
18
+ 'api[_-]?key'
19
+ 'access[_-]?token'
20
+ 'private[_-]?key'
21
+ 'auth[_-]?token'
22
+ 'client[_-]?secret'
23
+ 'bearer'
24
+
25
+ # 数据库连接串
26
+ 'jdbc:'
27
+ 'mysql://'
28
+ 'postgres://'
29
+ 'mongodb://'
30
+ 'redis://'
31
+
32
+ # 私钥文件
33
+ '-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY'
34
+
35
+ # 平台 Token
36
+ 'sk-[a-zA-Z0-9]{20,}' # OpenAI
37
+ 'ghp_[a-zA-Z0-9]{36}' # GitHub
38
+ 'AKIA[0-9A-Z]{16}' # AWS
39
+ )
40
+
41
+ # 拼接为正则
42
+ REGEX=$(IFS='|'; echo "${PATTERNS[*]}")
43
+
44
+ # 扫描暂存内容
45
+ RESULT=$(git diff --cached -U0 2>/dev/null | grep -inE "$REGEX" 2>/dev/null)
46
+
47
+ if [ -n "$RESULT" ]; then
48
+ echo "========================================"
49
+ echo " WARNING: Sensitive data detected"
50
+ echo "========================================"
51
+ echo ""
52
+ echo "$RESULT"
53
+ echo ""
54
+ echo "Please review before committing."
55
+ echo "========================================"
56
+ exit 2 # 暂停等待人工审批
57
+ fi
58
+
59
+ exit 0
@@ -0,0 +1,592 @@
1
+ # 后端安全编码规范
2
+
3
+ FastAPI + PostgreSQL + Redis + MinIO + DolphinDB 项目的安全编码规则。AI 生成或修改代码时,必须严格按照这些规则执行。
4
+
5
+ ## 一、ID 设计(防遍历攻击)
6
+
7
+ ### 核心原则
8
+ **数据库内部用自增 ID(性能),对外暴露 UUID(安全)。** 攻击者无法通过递增 ID 遍历所有数据。
9
+
10
+ ### 强制规则
11
+
12
+ ```python
13
+ # ✅ 正确 — ORM 模型同时有 id 和 uuid
14
+ import uuid as uuid_lib
15
+ from sqlalchemy import String
16
+ from sqlalchemy.orm import Mapped, mapped_column
17
+
18
+ class User(Base, TimestampMixin):
19
+ __tablename__ = "users"
20
+
21
+ id: Mapped[int] = mapped_column(primary_key=True)
22
+ uuid: Mapped[str] = mapped_column(String(36), default=lambda: str(uuid_lib.uuid4()), unique=True, index=True)
23
+ name: Mapped[str] = mapped_column(String(50))
24
+
25
+
26
+ # ✅ 正确 — 路由用 UUID 查询
27
+ @router.get("/users/{user_uuid}", response_model=UserResponse)
28
+ async def get_user(user_uuid: str, service: UserService = Depends(get_user_service)):
29
+ return await service.get_by_uuid(user_uuid)
30
+
31
+
32
+ # ✅ 正确 — Response Schema 只返回 uuid,不返回 id
33
+ class UserResponse(BaseModel):
34
+ uuid: str
35
+ name: str
36
+ # 不暴露 id 字段
37
+
38
+
39
+ # ❌ 违规 — 路由直接用自增 ID
40
+ @router.get("/users/{user_id}")
41
+ async def get_user(user_id: int): ...
42
+
43
+ # ❌ 违规 — Response 暴露自增 ID
44
+ class UserResponse(BaseModel):
45
+ id: int # 攻击者可遍历 1,2,3...
46
+ ```
47
+
48
+ ### 违规检测
49
+ 如果在路由路径中发现 `/{name}_id` 且类型为 `int`(如 `user_id: int`),必须提示用户改为 UUID。
50
+
51
+ ## 二、密码安全
52
+
53
+ ### 强制规则
54
+
55
+ ```python
56
+ # ✅ 正确 — bcrypt 存储
57
+ from danweiyuan_base.security import hash_password, verify_password
58
+
59
+ hashed = hash_password("user_password") # 存储到数据库
60
+ is_valid = verify_password("input", hashed) # 验证时比对
61
+
62
+ # ❌ 违规 — 明文存储
63
+ user.password = "123456"
64
+
65
+ # ❌ 违规 — MD5/SHA 存储(可被彩虹表破解)
66
+ user.password = hashlib.md5(password.encode()).hexdigest()
67
+
68
+ # ❌ 违规 — 接口返回密码字段
69
+ class UserResponse(BaseModel):
70
+ id: int
71
+ name: str
72
+ password: str # 绝对禁止
73
+ ```
74
+
75
+ | 规则 | 说明 |
76
+ |------|------|
77
+ | 存储算法 | **只允许 bcrypt**(或 argon2),禁止 MD5/SHA/明文 |
78
+ | 接口返回 | **永远不返回**密码字段,Response Schema 中不包含 |
79
+ | 密码强度 | 用户密码最少 8 位,管理员密码最少 12 位 |
80
+ | 登录错误 | 返回"用户名或密码错误",**不区分**是用户名不存在还是密码错误 |
81
+
82
+ ## 三、接口认证与授权
83
+
84
+ ### 强制规则
85
+
86
+ ```python
87
+ # ✅ 所有 API 必须 JWT 认证(白名单除外)
88
+ WHITELIST = ["/api/health", "/api/auth/login", "/api/auth/register"]
89
+
90
+ # ✅ 路由级别认证
91
+ @router.get("/users", dependencies=[Depends(get_current_user)])
92
+ async def list_users(): ...
93
+
94
+ # ✅ 角色检查
95
+ @router.delete("/users/{uuid}", dependencies=[Depends(require_admin)])
96
+ async def delete_user(uuid: str): ...
97
+
98
+ # ❌ 违规 — 无认证的数据接口
99
+ @router.get("/users")
100
+ async def list_users(): ...
101
+ ```
102
+
103
+ ### Token 安全
104
+
105
+ | 规则 | 说明 |
106
+ |------|------|
107
+ | Access Token 有效期 | **30 分钟**(最长不超过 2 小时) |
108
+ | Refresh Token 有效期 | **7 天** |
109
+ | Token 黑名单 | 登出后 token 必须失效(Redis 存黑名单,TTL = 剩余有效期) |
110
+ | Token 存储 | 前端存 `localStorage`(SPA),**禁止**存 Cookie 不设 HttpOnly |
111
+ | Secret Key | 从环境变量读取,最少 32 位随机字符串 |
112
+
113
+ ## 四、输入校验
114
+
115
+ ### 强制规则
116
+
117
+ ```python
118
+ # ✅ Pydantic Schema 严格校验
119
+ class UserCreate(BaseModel):
120
+ name: str = Field(min_length=2, max_length=50)
121
+ email: EmailStr
122
+ password: str = Field(min_length=8, max_length=128)
123
+ age: int = Field(ge=0, le=150)
124
+
125
+ # ✅ 列表查询参数限制
126
+ class ListParams(BaseModel):
127
+ page: int = Field(default=1, ge=1)
128
+ page_size: int = Field(default=20, ge=1, le=100) # 上限 100
129
+
130
+ # ❌ 违规 — 不校验直接使用
131
+ @router.post("/users")
132
+ async def create_user(data: dict): ...
133
+
134
+ # ❌ 违规 — page_size 无上限
135
+ class ListParams(BaseModel):
136
+ page_size: int = 20 # 攻击者可传 999999
137
+ ```
138
+
139
+ ### 校验规则清单
140
+
141
+ | 字段类型 | 校验 |
142
+ |---------|------|
143
+ | 字符串 | `min_length` + `max_length` |
144
+ | 数字 | `ge` + `le`(最小值 + 最大值) |
145
+ | 邮箱 | `EmailStr` |
146
+ | 手机号 | `pattern=r"^1[3-9]\d{9}$"` |
147
+ | URL | `HttpUrl` |
148
+ | 列表 | `max_length`(限制数组长度) |
149
+ | 文件大小 | 后端二次校验(不信任前端) |
150
+
151
+ ## 五、响应安全
152
+
153
+ ### 强制规则
154
+
155
+ ```python
156
+ # ✅ Response Schema 严格控制返回字段
157
+ class UserResponse(BaseModel):
158
+ uuid: str
159
+ name: str
160
+ email: str
161
+ is_active: bool
162
+ created_at: datetime
163
+
164
+ model_config = ConfigDict(from_attributes=True)
165
+
166
+ # ❌ 违规 — 直接返回 ORM 对象
167
+ @router.get("/users/{uuid}")
168
+ async def get_user(uuid: str, db=Depends(get_db)):
169
+ user = await db.get(User, uuid)
170
+ return user # 可能暴露 password、内部 ID 等
171
+
172
+ # ✅ 列表接口必须分页
173
+ @router.get("/users", response_model=PaginatedResponse[UserResponse])
174
+ async def list_users(params: PaginationParams = Depends()): ...
175
+
176
+ # ❌ 违规 — 无分页返回全量数据
177
+ @router.get("/users")
178
+ async def list_users():
179
+ return await db.execute(select(User)) # 全量泄露
180
+ ```
181
+
182
+ ### PII 脱敏
183
+
184
+ ```python
185
+ # ✅ 工具函数
186
+ def mask_phone(phone: str) -> str:
187
+ """138****5678"""
188
+ if len(phone) != 11:
189
+ return phone
190
+ return phone[:3] + "****" + phone[7:]
191
+
192
+ def mask_id_card(id_card: str) -> str:
193
+ """420***********1234"""
194
+ if len(id_card) != 18:
195
+ return id_card
196
+ return id_card[:3] + "***********" + id_card[14:]
197
+
198
+ # ✅ 在 Response Schema 中使用 validator 脱敏
199
+ class UserResponse(BaseModel):
200
+ phone: str
201
+
202
+ @field_validator("phone", mode="before")
203
+ @classmethod
204
+ def mask_phone_field(cls, v: str) -> str:
205
+ return mask_phone(v) if v else v
206
+ ```
207
+
208
+ ## 六、错误处理安全
209
+
210
+ ### 强制规则
211
+
212
+ ```python
213
+ # ✅ 生产环境只返回通用错误
214
+ raise NotFoundError("用户") # → {"code": "NOT_FOUND", "message": "用户不存在"}
215
+ raise BusinessError("操作失败") # → {"code": "UNKNOWN_ERROR", "message": "操作失败"}
216
+
217
+ # ❌ 违规 — 回显用户输入
218
+ raise BusinessError(f"未找到: {user_input}") # XSS/信息泄露
219
+
220
+ # ❌ 违规 — 暴露内部错误
221
+ except Exception as e:
222
+ return {"error": str(e)} # 可能包含 SQL、文件路径、堆栈
223
+
224
+ # ❌ 违规 — 暴露数据库结构
225
+ except IntegrityError as e:
226
+ return {"error": f"数据库错误: {e}"} # 暴露表名、字段名
227
+
228
+ # ✅ 正确 — 统一错误处理
229
+ except IntegrityError:
230
+ raise BusinessError("数据冲突,请检查是否重复提交", code="CONFLICT")
231
+ ```
232
+
233
+ ### 生产环境禁止
234
+
235
+ | 禁止返回的内容 | 原因 |
236
+ |--------------|------|
237
+ | 完整异常堆栈 | 暴露代码结构和文件路径 |
238
+ | SQL 语句 | 暴露表结构和查询逻辑 |
239
+ | 文件系统路径 | 暴露服务器目录结构 |
240
+ | 用户输入回显 | XSS 和信息泄露 |
241
+ | 数据库字段名 | 暴露数据模型 |
242
+ | 第三方 API Key | 凭证泄露 |
243
+
244
+ ## 七、注入防护
245
+
246
+ ### SQL 注入
247
+
248
+ ```python
249
+ # ✅ ORM 参数化查询(安全)
250
+ result = await session.execute(select(User).where(User.id == user_id))
251
+
252
+ # ✅ 原生 SQL 参数化(安全)
253
+ result = await session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
254
+
255
+ # ❌ 违规 — 字符串拼接(SQL 注入)
256
+ result = await session.execute(text(f"SELECT * FROM users WHERE id = {user_id}"))
257
+ ```
258
+
259
+ ### 命令注入
260
+
261
+ ```python
262
+ # ❌ 违规 — shell 命令注入
263
+ import os
264
+ os.system(f"convert {user_filename} output.png")
265
+
266
+ import subprocess
267
+ subprocess.run(f"ls {user_path}", shell=True)
268
+
269
+ # ✅ 正确 — 参数列表(不经过 shell)
270
+ subprocess.run(["convert", safe_filename, "output.png"], shell=False)
271
+
272
+ # ✅ 最佳 — 不调用外部命令,用 Python 库
273
+ from PIL import Image
274
+ img = Image.open(safe_path)
275
+ ```
276
+
277
+ ### 路径遍历
278
+
279
+ ```python
280
+ # ❌ 违规 — 用户控制文件路径
281
+ file_path = f"/uploads/{user_input_filename}"
282
+
283
+ # ✅ 正确 — 安全提取文件名
284
+ from pathlib import Path
285
+ safe_name = Path(user_input_filename).name # 去除 ../../../ 等
286
+ file_path = Path("/uploads") / safe_name
287
+ ```
288
+
289
+ ### SSRF(服务端请求伪造)
290
+
291
+ ```python
292
+ # ❌ 违规 — 用户控制 URL 发起请求
293
+ url = request_body.url # 用户传入
294
+ response = await httpx.get(url) # 可能访问内网 169.254.169.254
295
+
296
+ # ✅ 正确 — 白名单限制
297
+ ALLOWED_HOSTS = ["api.example.com", "cdn.example.com"]
298
+ parsed = urlparse(url)
299
+ if parsed.hostname not in ALLOWED_HOSTS:
300
+ raise BusinessError("不允许的目标地址")
301
+ ```
302
+
303
+ ## 八、软删除
304
+
305
+ ### 强制规则
306
+
307
+ ```python
308
+ # ✅ 模型添加软删除字段
309
+ class User(Base, TimestampMixin):
310
+ __tablename__ = "users"
311
+
312
+ id: Mapped[int] = mapped_column(primary_key=True)
313
+ is_deleted: Mapped[bool] = mapped_column(default=False)
314
+ deleted_at: Mapped[datetime | None] = mapped_column(default=None)
315
+
316
+ # ✅ 删除操作只更新标记
317
+ async def delete_user(self, user_uuid: str) -> None:
318
+ user = await self.get_by_uuid(user_uuid)
319
+ user.is_deleted = True
320
+ user.deleted_at = datetime.now(timezone.utc)
321
+ await self.db.commit()
322
+
323
+ # ✅ 查询时自动排除已删除
324
+ async def list_users(self) -> list[User]:
325
+ result = await self.db.execute(
326
+ select(User).where(User.is_deleted == False)
327
+ )
328
+ return list(result.scalars().all())
329
+
330
+ # ❌ 违规 — 物理删除
331
+ async def delete_user(self, user_id: int) -> None:
332
+ user = await self.db.get(User, user_id)
333
+ await self.db.delete(user) # 数据永久丢失
334
+ await self.db.commit()
335
+ ```
336
+
337
+ ### 例外
338
+ 以下场景允许物理删除(但需用户确认):
339
+ - 合规要求的数据清除(如 GDPR 被遗忘权)
340
+ - 临时数据清理(验证码、过期 session)
341
+
342
+ ## 九、审计日志
343
+
344
+ ### 强制规则
345
+
346
+ 需要记录审计日志的操作:
347
+
348
+ | 操作 | 记录内容 |
349
+ |------|---------|
350
+ | 登录成功/失败 | 用户名、IP、时间、User-Agent |
351
+ | 权限变更 | 操作人、目标用户、旧角色、新角色 |
352
+ | 数据删除 | 操作人、删除的资源类型和 UUID |
353
+ | 敏感数据查询 | 查询人、查询条件(脱敏) |
354
+ | 密码修改 | 操作人、时间 |
355
+ | 导出操作 | 操作人、导出数据范围 |
356
+
357
+ ```python
358
+ # ✅ 审计日志示例
359
+ import logging
360
+
361
+ audit_logger = logging.getLogger("audit")
362
+
363
+ async def delete_user(self, user_uuid: str, operator: dict) -> None:
364
+ user = await self.get_by_uuid(user_uuid)
365
+ user.is_deleted = True
366
+ await self.db.commit()
367
+
368
+ audit_logger.info(
369
+ "user_deleted operator=%s target=%s",
370
+ operator["uuid"],
371
+ user_uuid,
372
+ )
373
+ ```
374
+
375
+ ## 十、PostgreSQL 安全
376
+
377
+ ### 强制规则
378
+
379
+ | 规则 | 说明 |
380
+ |------|------|
381
+ | 参数化查询 | **禁止**字符串拼接 SQL,必须 ORM 或参数化 |
382
+ | 应用用户权限 | 应用数据库用户**只给** SELECT/INSERT/UPDATE/DELETE,**不给** DROP/CREATE/ALTER |
383
+ | 连接池限制 | `pool_size` 不超过 20,`max_overflow` 不超过 10 |
384
+ | SSL 连接 | 生产环境建议启用 SSL(`sslmode=require`) |
385
+ | 敏感字段加密 | 身份证号等字段使用 AES 加密存储,不只是展示时脱敏 |
386
+
387
+ ### 应用用户权限配置
388
+ ```sql
389
+ -- 创建应用专用用户(非超级用户)
390
+ CREATE USER app_user WITH PASSWORD 'strong_password';
391
+ GRANT CONNECT ON DATABASE mydb TO app_user;
392
+ GRANT USAGE ON SCHEMA public TO app_user;
393
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
394
+ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;
395
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
396
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app_user;
397
+
398
+ -- ❌ 禁止给应用用户的权限
399
+ -- GRANT ALL PRIVILEGES
400
+ -- SUPERUSER
401
+ -- CREATEDB
402
+ -- CREATEROLE
403
+ ```
404
+
405
+ ## 十一、Redis 安全
406
+
407
+ ### 强制规则
408
+
409
+ | 规则 | 说明 |
410
+ |------|------|
411
+ | 认证 | **必须设置密码**(`requirepass`),不允许无密码 Redis |
412
+ | 绑定地址 | `bind 127.0.0.1`,**禁止** `bind 0.0.0.0` |
413
+ | 危险命令 | **禁用** `FLUSHALL`/`FLUSHDB`/`CONFIG`/`KEYS`/`DEBUG` |
414
+ | Key TTL | 所有 key **必须设置过期时间**,禁止无 TTL 的 key |
415
+ | Key 前缀 | 所有 key 必须有项目前缀(如 `myapp:token:xxx`) |
416
+ | 存储内容 | **禁止**在 Redis 存储敏感原文(密码、身份证明文) |
417
+
418
+ ### Redis 配置
419
+ ```
420
+ # redis.conf
421
+ requirepass your_strong_password
422
+ bind 127.0.0.1
423
+ rename-command FLUSHALL ""
424
+ rename-command FLUSHDB ""
425
+ rename-command CONFIG ""
426
+ rename-command KEYS ""
427
+ rename-command DEBUG ""
428
+ maxmemory 256mb
429
+ maxmemory-policy allkeys-lru
430
+ ```
431
+
432
+ ### Key 设计规范
433
+ ```python
434
+ # ✅ 正确 — 有前缀、有 TTL
435
+ await redis.set("myapp:token:blacklist:abc123", "1", ex=3600)
436
+ await redis.set("myapp:cache:user:uuid123", json.dumps(user_data), ex=300)
437
+
438
+ # ❌ 违规 — 无前缀
439
+ await redis.set("abc123", "1")
440
+
441
+ # ❌ 违规 — 无 TTL
442
+ await redis.set("myapp:data:xxx", value) # 永不过期,内存泄漏
443
+
444
+ # ❌ 违规 — 存储敏感原文
445
+ await redis.set("myapp:user:password", "123456")
446
+ ```
447
+
448
+ ## 十二、MinIO / 文件存储安全
449
+
450
+ ### 强制规则
451
+
452
+ | 规则 | 说明 |
453
+ |------|------|
454
+ | Bucket 访问 | **默认私有**,不开放 public 读 |
455
+ | 文件访问 | 通过**预签名 URL** 限时访问(默认 15 分钟) |
456
+ | 文件名 | **UUID 重命名**,禁止保留用户原始文件名做存储路径 |
457
+ | 文件类型 | 校验文件 **magic bytes**(不只检查扩展名) |
458
+ | 文件大小 | 后端二次校验大小限制(不信任前端或 Nginx) |
459
+
460
+ ```python
461
+ # ✅ 正确 — UUID 重命名 + 预签名 URL
462
+ import uuid as uuid_lib
463
+ from minio import Minio
464
+
465
+ async def upload_file(file: UploadFile) -> str:
466
+ # 校验文件类型(magic bytes)
467
+ header = await file.read(8)
468
+ await file.seek(0)
469
+ if not is_allowed_file_type(header):
470
+ raise BusinessError("不允许的文件类型")
471
+
472
+ # UUID 重命名
473
+ ext = Path(file.filename).suffix
474
+ safe_name = f"{uuid_lib.uuid4()}{ext}"
475
+
476
+ client.put_object("uploads", safe_name, file.file, file.size)
477
+ return safe_name
478
+
479
+ def get_download_url(filename: str) -> str:
480
+ # 预签名 URL,15 分钟有效
481
+ return client.presigned_get_object("uploads", filename, expires=timedelta(minutes=15))
482
+
483
+
484
+ # ❌ 违规 — 用户原始文件名
485
+ safe_name = file.filename # 可能包含 ../../../etc/passwd
486
+
487
+ # ❌ 违规 — 公开 bucket
488
+ client.set_bucket_policy("uploads", public_read_policy)
489
+
490
+ # ❌ 违规 — 只检查扩展名
491
+ if file.filename.endswith(".jpg"): # 攻击者可改扩展名
492
+ ```
493
+
494
+ ## 十三、DolphinDB 安全
495
+
496
+ ### 强制规则
497
+
498
+ | 规则 | 说明 |
499
+ |------|------|
500
+ | 输入白名单 | 所有外部输入**必须经过正则白名单**验证后才能插入 DolphinDB 脚本 |
501
+ | 禁止拼接 | **禁止**将用户输入直接拼接到 DolphinDB 脚本字符串中 |
502
+ | 连接认证 | DolphinDB 连接**必须使用账号密码**,从环境变量读取 |
503
+ | 端口隔离 | DolphinDB 端口**禁止对外暴露**,仅 Docker 内部网络或 127.0.0.1 |
504
+
505
+ ```python
506
+ # ✅ 正确 — 白名单验证后再插入脚本
507
+ import re
508
+
509
+ SAFE_COLUMN_NAME = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]{0,63}$')
510
+ SAFE_TABLE_NAME = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]{0,63}$')
511
+
512
+ def validate_column_name(name: str) -> str:
513
+ if not SAFE_COLUMN_NAME.match(name):
514
+ raise BusinessError(f"非法字段名: {name}")
515
+ return name
516
+
517
+ # 使用
518
+ col = validate_column_name(user_input_column)
519
+ script = f"select {col} from loadTable('dfs://mydb', 'mytable')"
520
+
521
+
522
+ # ❌ 违规 — 直接拼接用户输入
523
+ script = f"select * from loadTable('dfs://mydb', '{user_input}')" # DolphinDB 注入
524
+
525
+ # ❌ 违规 — 无验证
526
+ factor_name = request.query_params.get("factor")
527
+ script = f"select * from factors where factor_name = '{factor_name}'"
528
+ ```
529
+
530
+ ### DolphinDB 连接安全
531
+ ```python
532
+ # ✅ 正确 — 从环境变量读取
533
+ ddb_conn = ddb.Session()
534
+ ddb_conn.connect(
535
+ host=settings.dolphindb_host,
536
+ port=settings.dolphindb_port,
537
+ userid=settings.dolphindb_user,
538
+ password=settings.dolphindb_password,
539
+ )
540
+
541
+ # ❌ 违规 — 硬编码
542
+ ddb_conn.connect("localhost", 8848, "admin", "123456")
543
+ ```
544
+
545
+ ## 十四、API 限流
546
+
547
+ ### 强制规则
548
+
549
+ 不同接口使用不同的限流策略:
550
+
551
+ | 接口类型 | 限流 | 原因 |
552
+ |---------|------|------|
553
+ | 登录 `/auth/login` | 3 次/分钟/IP | 防暴力破解 |
554
+ | 注册 `/auth/register` | 5 次/分钟/IP | 防批量注册 |
555
+ | 普通查询 | 60 次/分钟/IP | 防爬虫 |
556
+ | 数据导出 | 5 次/分钟/IP | 防数据泄露 |
557
+ | 文件上传 | 10 次/分钟/IP | 防资源滥用 |
558
+
559
+ ```python
560
+ # ✅ 使用 slowapi 限流
561
+ from slowapi import Limiter
562
+ from slowapi.util import get_remote_address
563
+
564
+ limiter = Limiter(key_func=get_remote_address)
565
+
566
+ @router.post("/auth/login")
567
+ @limiter.limit("3/minute")
568
+ async def login(request: Request, body: LoginRequest): ...
569
+
570
+ @router.get("/users/export")
571
+ @limiter.limit("5/minute")
572
+ async def export_users(request: Request): ...
573
+ ```
574
+
575
+ ## 十五、违规检测清单
576
+
577
+ AI 在编写或审查代码时,必须检查以下违规模式:
578
+
579
+ | 检查项 | 违规模式 | 严重程度 |
580
+ |--------|---------|---------|
581
+ | 自增 ID 暴露 | 路由参数为 `int` 类型的 ID | 高 |
582
+ | 密码明文 | `password` 字段出现在 Response Schema | 致命 |
583
+ | 密码弱存储 | 使用 MD5/SHA 而非 bcrypt | 致命 |
584
+ | 无认证接口 | 数据接口无 `Depends(get_current_user)` | 高 |
585
+ | SQL 拼接 | `f"SELECT ... {user_input}"` | 致命 |
586
+ | 命令注入 | `os.system()` / `subprocess(shell=True)` | 致命 |
587
+ | 全量返回 | 列表接口无分页限制 | 高 |
588
+ | 错误回显 | `str(exception)` 返回给客户端 | 高 |
589
+ | 物理删除 | `session.delete()` 用于业务数据 | 中 |
590
+ | Redis 无 TTL | `redis.set()` 没有 `ex` 参数 | 中 |
591
+ | 文件名不安全 | 使用用户原始文件名做存储路径 | 高 |
592
+ | DolphinDB 拼接 | 用户输入直接进 DolphinDB 脚本 | 致命 |