Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rest): further sanitize json parsing by rejecting prohibited keys #6676

Merged
merged 1 commit into from Nov 5, 2020

Conversation

raymondfeng
Copy link
Contributor

@raymondfeng raymondfeng commented Oct 28, 2020

  • reject proto and constructor by default
  • allow json parse options to provide additional prohibitedKeys

Signed-off-by: Raymond Feng enjoyjava@gmail.com

Checklist

  • DCO (Developer Certificate of Origin) signed in all commits
  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

馃憠 Check out how to submit a PR 馃憟

expect(() => parseJson(text)).to.throw(
'JSON string cannot contain "constructor.prototype" key.',
);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think string contain constructor.prototype like:

const text = '{"x": "1", "constructor.prototype.y": {"z": 2}}'

is also a valid case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to block constructor.prototype.y as a key?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally people use JSON.stringify(someObject) to create a string. I've never seen a string with proto and constructor.prototype etc. So are you adding checks in parseJson(text) in case someone types the string completely by hand without using JSON.stringify() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emonddr This PR is to sanitize user input from API clients with bad intention to exploit the server vulnerabilities such as prototype pollution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raymondfeng I tested the following example in chrome:

var myObj = {};
myObj.constructor.prototype.foo = "bar"
"bar"
console.log({}.foo)
VM373:1 bar

so I think pattern constructor.prototype.* does pollute?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I believe your sanitizeJsonParse function checks this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two different things:

  1. myObj.constructor.prototype // nesting properties constructor/prototye
  2. myObj['constructor.prototype'] // a special property named constructor.protoyype

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, got the difference 馃憤

if (
key === 'constructor' &&
value != null &&
Object.prototype.hasOwnProperty.call(value, 'prototype')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check the value's field contains prototype as well? like:

const text = '{"x": "1", "y": {"constructor": {"prototype.z": {"foo": 2}}}}';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, only {"constructor": {"prototype": ...}} is dangerous.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, only {"constructor": {"prototype": ...}} is dangerous.

Why is that dangerous? If you could clarify. thanks :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See https://medium.com/node-modules/what-is-prototype-pollution-and-why-is-it-such-a-big-deal-2dd8d89a93c

Yikes. The fact that it pollutes all objects after one string is parsed...wow. Thanks for the link.

@raymondfeng
Copy link
Contributor Author

@bajtos Can you review the PR?

@emonddr
Copy link
Contributor

emonddr commented Oct 30, 2020

@raymondfeng , perhaps a test where an application actually sets

RestBindings.REQUEST_BODY_PARSER_OPTIONS

with options containing the prohibited_keys array ?

In the current test cases, options below is always {} .

export class JsonBodyParser implements BodyParser {
  name = builtinParsers.json;
  private jsonParser: BodyParserMiddleware;

  constructor(
    @inject(RestBindings.REQUEST_BODY_PARSER_OPTIONS, {optional: true})
    options: RequestBodyParserOptions = {},
  ) {
    const jsonOptions = getParserOptions('json', options);
    jsonOptions.reviver = sanitizeJsonParse(
      jsonOptions.reviver,
      jsonOptions.prohibitedKeys,
    );
    this.jsonParser = json(jsonOptions);
  }

It would be nice to see

export function sanitizeJsonParse(
  reviver?: (key: any, value: any) => any,
  prohibitedKeys?: string[],
) {
  prohibitedKeys = [
    '__proto__',
    'constructor.prototype',
    ...(prohibitedKeys ?? []),
  ];

exercised in this complete path. Just a thought.

@raymondfeng raymondfeng force-pushed the protect-json-parse branch 2 times, most recently from 4209959 to 5f7cbb2 Compare October 30, 2020 21:56
- reject `__proto__` and `constructor.prototype` by default
- allow validation options to provide additional prohibitedKeys

Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
throw new Error('JSON string cannot contain "__proto__" key.');
if (key === '__proto__') {
// Reject `__proto__`
throw new Error(`JSON string cannot contain "${key}" key.`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it okay throws an Error? i think this returns a 500 error and could stop the application may be should throw a 422 (Unprocessable Entity)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos
Copy link
Member

bajtos commented Nov 5, 2020

Personally, I would prefer to leverage an existing solution from Node.js/npm ecosystem instead of inventing our own solution. Few packages to consider:

@raymondfeng
Copy link
Contributor Author

@bajtos Personally, I would prefer to leverage an existing solution from Node.js/npm ecosystem instead of inventing our own solution. Few packages to consider:

I would love to use https://github.com/fastify/secure-json-parse but it does not allow us to filter out keys such as __proto__.xyz or constructor.prototype, which are usually not concerning but they do cause issues if a property path is used to retrieve nesting properties.

@raymondfeng
Copy link
Contributor Author

@bajtos Since it's a security concern, we should probably land and release the PR for now and improve it overtime.

@raymondfeng raymondfeng merged commit b38f0fd into master Nov 5, 2020
@raymondfeng raymondfeng deleted the protect-json-parse branch November 5, 2020 20:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants