Egg: [RFC] 给 ctx.request.body, ctx.request.query, ctx.request.params 增加一个类定义

Created on 8 Mar 2018  ·  20Comments  ·  Source: eggjs/egg

背景描述

由于这几个用户请求参数非常方便,开发者常常会偷懒,直接透传这些参数到 Model 层,直接作为 Model 层参数创建数据对象,然后保存的数据库。示例代码如下:

ctx.model.Post.create(ctx.request.body);
ctx.model.Post.update(ctx.request.query.id, ctx.request.body);
ctx.model.Post.destroy(ctx.request.params);

这样看起来代码很简单优雅,实际隐含着非常容易被攻击的可能性。如这 create 的时候,攻击者可以给 body 设置 id,强制修改你以为的自增长 id。

或者修改 user_id, role 之类的重要字段,导致漏洞

这种模式违反了一个安全原则:不能信任一切用户输入的参数。

解决方案

借鉴 Rails 的 StrongParameters 思路,如果这些参数都是基于统一的 Parameters 参数类生产的参数对象,那么就可以这 Model 层的 create,update 和 destroy 各种数据操作方法里面,实现对参数判断,确保这些参数都不能够是 Parameters 类的对象实例。

Model.prototype.create = function(params) {
  if (params instanceof Parameters) throw new TypeError('wrong params');
  // ...
};

或者判断 params.permitted,总之需要给 Model 层知道参数是否原始用户参数,还是经过开发者处理过的参数。

egg-security proposals

Most helpful comment

All 20 comments

cc @huacnlee

一般入参不是都要校验的吗

@popomore 校验了,但是多传的参数,你不会去校验。

可以参考最早 Rails Strong Parameters 发布时候的公告:


Rails 里面实现是 if (!params.permitted?) { throw new error }

当调用过 params.permit 以后会改变状态,params 依然是 Parameters 的 Instance,这样的好处是 Parameters 里面的其他方法(它比标准的 Hash 带有更多实用功能,还能扩展)还能继续使用,例如 merge

@huacnlee 可以返回 StrongParameters 类,它是 Parameters 的子类,Parameters 的其他方法都继承了。

使用ts的话,应该是一个以 model 形式的 interface 来限制传参,多余的参数被过滤掉,这样可以避免

@huacnlee StrongParameters 类不行,看来还是得判断是否明确被 permit 执行过。

主要是你拦不住别人,或你自己大意的时候,这么使了一把:

yield ctx.model.Topic.create(ctx.request.body)
// 或者更复杂的场景
const topicParam = Object.assign(ctx.request.query, ctx.request.body);
topicParam = Object.assign({
  // ... 一些其他复杂的额外参数
}, topicParam)
// ... 好多行代码
// 然后后面这行你就一眼看不出毛病了
// 或者一开始前面是 _.pick 出来的,Review 通过了
// 某天不知道被谁改了,变成直接用 ctx.request.body,Diff 上你看不到后面是直接塞 topicParam 到 Model 的
yield ctx.model.Topic.create(topicParam);

@huacnlee 核心原则就是要让 Model 感知参数是否经过开发者处理。

另外,不应该直接过滤,而是应该跑异常,让开发者在实现过程中就能发现问题。
确保开发者是显示的提取参数的。

可以用 json schema 来验证,不会有多余值

ts 或者 json schema 都可以做到,但是架不住人偷懒。最终还是人性问题。

For example:

https://github.com/eggjs/examples/blob/master/sequelize-example/app/controller/post.js#L20
https://github.com/eggjs/examples/blob/master/sequelize-example/app/service/post.js#L36

以及其他的用户

https://github.com/malun666/hamkd/blob/45a3e83716853092dba368dd71a73e8257fe97ae/app/controller/user.js#L50
https://github.com/jjj201200/blog/blob/28b4d719e910ad2eb8639c0ae5c141d400f23f7b/app/service/blog.js#L78

这不是懒、意识的原因。

而是某些时候你在写这行的时候,你不会想到那么多细节,尤其是事情多,项目复杂,功能多的时候

Strong Parameters 的机制就是你必须制定要传递的属性,否则代码跑不通。
开发者不用时刻注意各种各样的细节,有问题框架会提示出来。

人都会偷懒,确实拦不住,但是可以设置强校验,每一个字段都【恶心】开发者 Permit 一下的话,至少可以有一道防线拦一下或者给开发者敲一下钟

是不是可以增加一个类似 Laravelfillable (可被批量赋值的属性设置) / guarded (不可被批量赋值的属性设置) ,来对创建、更新的数据进行过滤限制呢?

公司业务的代码我会检查 body 中的参数,但是确实没有找到 best practice。写 sequelize-example 的时候确实是偷懒了,赞成这个。

是否可以给 Controller 基类定义 permit 函数
结合使用 egg-validate

class BaseController {
  permit(validateSchema, requestObj) {
    const ctx = this.ctx;
    ctx.validate(obj, requestObj);
    const targetObj = {};
    for (const key in requestObj) {
      targetObj[key] = requestObj[key];
    }

    return targetObj;
  }
}

// 业务 Controller

* index() {
  const createParam = this.permit({
    type: { type: 'string' },
  }, ctx.request.body);

  console.log(createParam.type);
  ....
}

使用ts的话,应该是一个以 model 形式的 interface 来限制传参,多余的参数被过滤掉,这样可以避免

这是一个很好的办法,学习了!

Was this page helpful?
0 / 5 - 0 ratings