☰
express-validator v5 数据清洗(Sanitization)实战指南:从 trim、escape 到自定义 Sanitizer
2026/10/10 5:26:12 网站建设 项目流程
  • 后端

【免费下载链接】express-validator

An express.js middleware for validator.js.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载

导读

在 HTTP 请求处理中,数据校验负责确认"格式是否正确",而数据清洗(Sanitization)则负责让数据"去除噪声"——例如去掉首尾空格、转义 HTML 特殊字符、规范化邮箱、把字符串转换为数字或布尔值。本文以 express-validator 5.3.0 版本的官方 Sanitization 文档为主线,结合仓库源码,完整讲解如何在同一条链上组合校验与清洗、如何用express-validator/filter下的sanitize/sanitizeBody等入口处理未参与校验的字段、清洗链的执行顺序与请求可变性,以及如何编写自定义 Sanitizer。读完本文,你将能安全、规范地处理来自 body、query、params、cookies 等位置的原始输入。

为什么需要 Sanitization

接收 HTTP 请求数据,不仅要保证数据格式正确,还要保证它没有噪声。例如用户在评论区输入了" Hello world :>) ",如果不做任何处理,这段内容会被原样存储并在页面上直接渲染,带来 XSS 风险与存储脏数据问题。

validator.js 提供了一批开箱即用的 Sanitizer,express-validator 将这些方法全部透传到清洗链上,让开发者可以在校验的同时顺手完成数据清洗,从而减少"校验过、但没清理过"的脏数据进入业务逻辑。

在 5.3.0 版本中,express-validator 的 API 按职责分为两个入口(这是 v5 的典型用法):

  • express-validator/check:提供check、body、query、params、cookie、header等校验链构建器;
  • express-validator/filter:提供sanitize、sanitizeBody、sanitizeCookie、sanitizeParam、sanitizeQuery、buildSanitizeFunction等清洗入口,以及matchedData。

在校验链上直接串联清洗方法

清洗最常见、最省事的做法是:对于已经被校验的字段,直接在同一个链上追加清洗方法。因为校验链本身就是中间件,清洗方法只是追加到同一上下文中的一个个清洗步骤。

来看官方文档的核心示例(feature-sanitization.md):

const express = require('express'); const { body } = require('express-validator/check'); const { sanitizeBody } = require('express-validator/filter'); const app = express(); app.use(express.json()); app.post('/comment', [ body('email') .isEmail() .normalizeEmail(), body('text') .not().isEmpty() .trim() .escape(), sanitizeBody('notifyOnReply').toBoolean() ], (req, res) => { // Handle the request somehow });

这段代码说明了两种典型场景:

  1. 已校验字段:email和text参与了校验,因此在同一条链上直接追加normalizeEmail()、trim()、escape()即可完成清洗,无需额外中间件;
  2. 未校验字段:notifyOnReply不需要校验,但需要把表单提交的字符串(如"true"/"on"/"1")转换成 JavaScript 布尔值,此时使用 filter API 中的sanitizeBody单独建立清洗链。

重要:Sanitization 会就地修改请求

官方文档特别强调了一条关键语义(原文以 "Important" 提示):

sanitization mutates the request.

也就是说,清洗不是"返回一个新值",而是直接改写请求对象中的字段。例如req.body.text原本是" Hello world :>) ",经过trim().escape()之后,它的值就变成了"Hello world :>)"。后续中间件与路由处理器读到的都是清洗后的值。

这一点与 api-sanitization-chain.md 中的说明一致:清洗链是中间件,运行时"就地修改每个字段,按指定顺序依次应用每个 Sanitizer"。

filter API:处理未参与校验的字段

当字段不参与校验、或希望独立管理清洗逻辑时,使用 filter API(require('express-validator/filter'))提供的清洗入口。

sanitize(fields) 与位置限定变体

  • sanitize(fields):fields为字段名字符串或字符串数组,返回一个 Sanitization Chain。它会在以下请求对象中查找并清洗字段:

    • req.body
    • req.cookies
    • req.params
    • req.query
    • 注意:req.headers目前不被支持。

    如果同一字段出现在多个位置(例如req.body.id和req.query.id同时存在),则该字段的所有实例都会被清洗。

  • sanitizeBody(fields):等同于sanitize(fields),但只清洗req.body;

  • sanitizeCookie(fields):只清洗req.cookies;

  • sanitizeParam(fields):只清洗req.params;

  • sanitizeQuery(fields):只清洗req.query。

buildSanitizeFunction(locations):自定义位置组合

如果需要在多个位置执行同一清洗逻辑,可以用buildSanitizeFunction(locations)生成定制版sanitize(),其中locations是body、cookies、params、query的任意组合:

const { buildSanitizeFunction } = require('express-validator/filter'); const sanitizeBodyAndQuery = buildSanitizeFunction(['body', 'query']); app.put('/update-product', [ // id 无论来自 req.body 还是 req.query,都会被转换为 int sanitizeBodyAndQuery('id').toInt() ], productUpdateHandler);

matchedData(req[, options]):只取清洗/校验后的数据

filter API 还提供了matchedData(req[, options]),它从请求中提取通过 check API 校验的数据并组装成对象,支持嵌套路径与通配符。可选参数:

  • includeOptionals:设为true时包含可选字段数据,默认false;
  • onlyValidData:设为false时包含未通过校验字段的数据,默认true;
  • locations:指定提取位置(body、cookies、headers、params、query),默认undefined表示所有位置。
// 假设请求为: // req.query = { from: '2017-01-12' } // req.body = { to: '2017-31-12' } app.post('/room-availability', check(['from', 'to']).isISO8601(), (req, res, next) => { const queryData = matchedData(req, { locations: ['query'] }); const bodyData = matchedData(req, { locations: ['body'] }); const allData = matchedData(req); console.log(queryData); // { from: '2017-01-12' } console.log(bodyData); // { to: '2017-31-12' } console.log(allData); // { from: '2017-01-12', to: '2017-31-12' } });

清洗链:顺序即结果

清洗链与校验链一样是中间件,必须传给 Express 路由处理函数。Sanitizer 按声明顺序依次执行,前一个的输出是后一个的输入:

app.get('/', sanitizeBody('trimMe').trim(), (req, res, next) => { // 如果 req.body.trimMe 原本是 " something " // 清洗后的值将是 "something" console.log(req.body.trimMe); });

从源码看清洗链的执行机制

从源码结构看,清洗步骤最终会被包装成 Sanitization 上下文项,挂在上下文构建器(ContextBuilder)上,随中间件运行:

export class Sanitization implements ContextItem { constructor( private readonly sanitizer: StandardSanitizer | CustomSanitizer, private readonly custom: boolean, private readonly options: any[] = [], ... ) {} async run(context: Context, value: any, meta: Meta) { ... if (this.custom) { const newValue = await runCustomSanitizer(); context.setData(path, newValue, location); return; } const values = Array.isArray(value) ? value : [value]; const newValues = values.map(value => { return (this.sanitizer as StandardSanitizer)(this.stringify(value), ...this.options); }); context.setData(path, values !== value ? newValues[0] : newValues, location); } }

由 sanitization.spec.ts 中的测试可以印证几个关键行为:

  • 清洗结果会写回上下文(并最终写回请求):测试断言context.setData以清洗后的新值被调用(见persists sanitized value back into the context);
  • 数组字段逐个清洗:标准 Sanitizer 对数组中的每一项分别调用(测试calls it for each item in array field验证了[1, 42]会各调用一次);
  • 标准 Sanitizer 收到的是字符串化后的值:在调用标准 Sanitizer 前,字段值会先经过 utils.ts 的 toString 处理——Date 转为 ISO 字符串,对象调用其toString(),null/undefined/NaN 转为空字符串;
  • 自定义 Sanitizer 收到(value, meta):meta中包含{ req, location, path }等上下文信息,且支持返回 Promise(源码中通过Promise.resolve包装);
  • 标准 Sanitizer 可接收附加参数:这些参数作为options原样透传(测试calls it with the options验证了这一点)。

清洗链上的完整 Sanitizer 清单

从 Sanitizers 接口定义 与 SanitizersImpl 实现 看,5.3.0 的清洗链除了透传 validator.js 的标准清洗方法外,还额外提供了两类便捷方法:

标准清洗(来自 validator.js,按声明顺序执行):

方法作用可选参数
blacklist(chars)删除字符串中出现在黑名单字符集里的字符chars必填
escape()HTML 转义(<、>、&、"、'等)—
unescape()HTML 反转义—
ltrim(chars?)去除左侧空白(或指定字符集)chars
rtrim(chars?)去除右侧空白(或指定字符集)chars
trim(chars?)去除两侧空白(或指定字符集)chars
normalizeEmail(options?)规范化邮箱(小写、去除点号别名等)NormalizeEmailOptions
stripLow(keep_new_lines?)去除 ASCII 控制字符keep_new_lines
toBoolean(strict?)转换为布尔值strict
toDate()尝试转换为 Date—
toFloat()转换为浮点数—
toInt(radix?)转换为整数radix
toLowerCase()/toUpperCase()大小写转换(仅对字符串生效,非字符串原样返回,见实现)—
whitelist(chars)仅保留白名单字符chars必填

便捷/自定义清洗(express-validator 自身实现):

  • default(default_value):当字段值为''、null、undefined或NaN时,替换为default_value(通过_.cloneDeep深拷贝,避免引用共享);
  • replace(values_to_replace, new_value):把指定值(可传数组)替换为new_value;
  • toArray():把非数组值包装为单元素数组(undefined变为空数组);
  • customSanitizer(sanitizer):见下文。

customSanitizer:自定义清洗逻辑

当 validator.js 自带的清洗方法不够用时,使用.customSanitizer(sanitizer)编写自定义清洗:

app.get('/object/:id', sanitizeParam('id').customSanitizer((value, { req }) => { return req.query.type === 'user' ? ObjectId(value) : Number(value); }), objectHandler);

签名说明(api-sanitization-chain.md):

  • sanitizer(value, { req, location, path }):接收被清洗字段的值,以及包含 Express 请求对象、位置(body/query/params/cookies)和字段路径的 meta;
  • 必须同步返回新值(源码实现中虽以Promise.resolve包装返回值,但文档明确指出当前要求同步函数)。

更多自定义清洗示例(如转换为 MongoDB ObjectID)可参考 feature-custom-validators-sanitizers.md。

通配符(Wildcards):批量清洗数组与对象字段

如果要对一个数组的所有元素或某个对象的全部键应用同一清洗规则,可以使用*通配符(详见 feature-wildcards.md)。

例如,校验所有地址的邮政编码合法,并把每个地址的number字段清洗为整数:

const express = require('express'); const { check } = require('express-validator/check'); const { sanitize } = require('express-validator/filter'); const app = express(); app.use(express.json()); app.post('/addresses', [ check('addresses.*.postalCode').isPostalCode(), sanitize('addresses.*.number').toInt() ], (req, res) => { // Handle the request });

它既能处理数组形式的地址列表:

{ "addresses": [ { "postalCode": "2010", "number": "500" }, { "postalCode": "", "number": "501" } ] }

也能处理键名预定义的对象形式:

{ "addresses": { "home": { "postalCode": "", "number": "501" }, "work": { "postalCode": "2010", "number": "500" } } }

配合前述 Sanitization 实现 中"对数组逐项清洗"的行为,sanitize('addresses.*.number').toInt()会把每个地址的number字符串转换为整数;而校验失败(如postalCode为空字符串)的地址也不会被清洗跳过——校验与清洗在各自链上独立处理。

完整实践建议

  1. 已校验字段:把清洗方法直接串在check/body/query等校验链上,减少中间件数量,并保证"只有通过校验的数据才会被清洗";
  2. 未校验字段:使用 filter API 的sanitize/sanitizeBody/sanitizeQuery/sanitizeParam/sanitizeCookie单独建链,或使用buildSanitizeFunction组合多个位置;
  3. 牢记顺序:trim().escape()与escape().trim()结果可能不同,Sanitizer 严格按声明顺序执行;
  4. 牢记就地修改:清洗会改写req,后续中间件读到的都是清洗后的值;如需原始值,应在清洗前自行保存;
  5. 自定义需求:优先组合标准 Sanitizer;不够用时用customSanitizer(同步)、default或replace补齐。

小结

数据清洗与数据校验是同一枚硬币的两面:校验保证"正确",清洗保证"干净"。express-validator 5.3.0 通过express-validator/filter的清洗入口与校验链上的内联清洗方法,将 validator.js 的 Sanitizer 能力完整带入 Express 中间件体系,并在源码层面通过 Sanitization 上下文项 实现了"逐字段、逐项、按序、就地"的清洗语义。掌握本文介绍的链式组合、位置限定、通配符与自定义 Sanitizer 四类用法,即可在真实项目中构建可靠的输入净化管线。

  • 后端

【免费下载链接】express-validator

An express.js middleware for validator.js.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载

相关推荐

上一篇:Pinpoint监控指标告警聚合规则:动态阈值调整终极指南
下一篇:从源码到APK:android-ffmpeg静态库在Android应用中的集成实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询