← 中文资源

简体中文版本

Mass Assignment 批量赋值

mass assignment 把请求字段直接绑定进模型属性,客户端可设置端点从未打算暴露的特权字段——role、balance、owner_id——构成权限提升。

阅读英文原文

概念定义

“用请求键值袋填充对象”很顺手也很危险:客户端控制键,模型有许多可设属性,交集里就有业务从没想暴露的字段。GitHub 2012 的公钥更新端点接受 admin 标志,是教科书案例。

工作机制

框架把 JSON 键映射到 setter 或列。防御要倒过来:每个端点声明精确接受的字段——strong parameters、显式 DTO、模式校验——未知键被拒绝或忽略而不是合并;所有权与租户字段一律从认证主体推导,绝不信请求体。

使用场景

审计每个创建与更新处理器,尤其是共享 DTO 的管理相邻流程。契约测试应提交带毒的额外键,并断言响应与持久化实体都不含它们。

常见误区

输出端隐藏字段只管响应,输入绑定才是攻击面;ORM 默认绑定全部列,防它必须显式 permit。嵌套对象会把字段藏到比白名单更深一层;管理员与自助服务共用一个 DTO 终会并集膨胀,应拆分。审计日志记录被拒键,可作为探测信号。

代码示例

import express from 'express';
import jwt from 'jsonwebtoken';

function requireAuthMiddleware(req, res, next) {
  const header = req.headers.authorization ?? '';
  const token = header.startsWith('Bearer ') ? header.slice(7) : null;
  if (!token) return res.status(401).json({ error: 'token required' });
  try {
    req.claims = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(403).json({ error: 'invalid token' });
  }
}

function requireRoleMiddleware(...allowed) {
  return (req, res, next) => {
    const role = req.claims?.role;
    if (!role || !allowed.includes(role)) {
      return res.status(403).json({ error: 'role ' + role + ' not permitted' });
    }
    next();
  };
}

const app = express();

app.delete('/invoices/:id',
  requireAuthMiddleware,
  requireRoleMiddleware('admin', 'finance'),
  (req, res) => res.json({ deleted: req.params.id }),
);

app.get('/profile',
  requireAuthMiddleware,
  (req, res) => res.json({ userId: req.claims.sub }),
);

app.listen(3000);
package main

import (
	"crypto/sha256"
	"crypto/subtle"
	"fmt"
)

func ConstantTimeEqual(a, b string) bool {
	left := sha256.Sum256([]byte(a))
	right := sha256.Sum256([]byte(b))
	return subtle.ConstantTimeCompare(left[:], right[:]) == 1
}

func main() {
	secret := "s3cr3t-token"
	fmt.Println(ConstantTimeEqual(secret, "s3cr3t-token"))
	fmt.Println(ConstantTimeEqual(secret, "s3cr3t-tokeX"))
	fmt.Println(ConstantTimeEqual(secret, "short"))
}