ca7595b113
- 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
52 lines
1.4 KiB
Go
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 "对象"
|
|
}
|
|
}
|