Files
jiu/backend/internal/handler/ownership.go
T
wangjia ca7595b113 feat(backend): 出入库汇总近30天滚动窗口 + 跨租户安全加固
- summaryBounds:本月/近30天滚动双口径(stock-in/out Summary ?window=rolling30)
- security(SEC-001):新增 ownership.go ensureShopRef 写入侧防线(stock-in/out/finance/
  盘点建单的 warehouse/partner/product 外键归属校验);读取侧全部 Preload 补
  shop_id 作用域,finance Summary JOIN 补租户条件;回归测试 CrossTenantRefs
- security(SEC-002):release 模式 JWT 密钥为空/默认值时拒绝启动
- gofmt 对齐若干 model/cmd 文件

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 09:57:52 +08:00

52 lines
1.4 KiB
Go

package handler
import (
"fmt"
"gorm.io/gorm"
)
// SEC-001(跨租户信息泄露)防线:
//
// 关联对象(Partner/Warehouse/Product…)若只按外键回连、不再按 shop_id 过滤,
// 攻击者可在本店单据里塞他店 id,再从 GET 接口把他店对象(含 PII)读出来。
// 两道防线缺一不可:
// 1. 写入侧 ensureShopRef:拒绝引用不属于当前店的外键;
// 2. 读取侧 Preload/JOIN 一律附加 shop_id 条件(存量脏引用也带不出数据)。
// ensureShopRef 校验外键对象存在且属于当前店(table 为调用点硬编码常量,非用户输入)。
func ensureShopRef(db *gorm.DB, table string, id uint64, shopID uint64) error {
if id == 0 {
return fmt.Errorf("引用对象 id 无效")
}
var n int64
db.Table(table).
Where("id = ? AND shop_id = ? AND deleted_at IS NULL", id, shopID).
Count(&n)
if n == 0 {
return fmt.Errorf("引用的%s不存在", refName(table))
}
return nil
}
// ensureShopRefOpt 可选外键(如 partner_id 指针):nil/0 视为未引用,直接通过。
func ensureShopRefOpt(db *gorm.DB, table string, id *uint64, shopID uint64) error {
if id == nil || *id == 0 {
return nil
}
return ensureShopRef(db, table, *id, shopID)
}
func refName(table string) string {
switch table {
case "partners":
return "往来单位"
case "warehouses":
return "仓库"
case "products":
return "商品"
default:
return "对象"
}
}