Skip to content

Secure operators - #8240

Merged
eseliger merged 28 commits into
sequelize:masterfrom
yonjah:secure-operators
Sep 30, 2017
Merged

Secure operators#8240
eseliger merged 28 commits into
sequelize:masterfrom
yonjah:secure-operators

Conversation

@yonjah

@yonjah yonjah commented Sep 1, 2017

Copy link
Copy Markdown
Contributor

Pull Request check-list

Please make sure to review and check all of these items:

  • Does npm run test or npm run test-DIALECT pass with this change (including linting)?
  • Does the description below contain a link to an existing issue (Closes #[issue]) or a description of the issue you are solving?
  • Have you added new tests to prevent regressions?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?
  • Did you follow the commit message conventions explained in CONTRIBUTING.md?

Description of change

This PR will convert all opeators to be used internally as Symbols.
This will reduce the possibility of operators injection as described in #7310

For backward compatibility all previous operators are available as aliases, but user are discourage from using those aliases and a deprecation warning will be logged unless user explicitly set her own aliases as described in the updated documentation

this PR also include changes from #8068 since it would have been impossible to implement without those changes

Fixes #7310

yonjah added 5 commits August 4, 2017 11:03
…ry-generator whereItemQuery by

whereItemQuery had cyclic complexity of 85 reduced to 26 to make logic a bit easier to follow.
Logic is almost identical but at places where logic looked like an obvoius error it was tweeked, like -
	_traverseJSON had weird logic if the item was plain object -
		cast wast set by either path (ok) or the first value of the first property of an object (doesn't make sense)
		where value passed into whereItemQuery was in the main function scope causing it to accumulate properties on each iteration (doesn't make sense)
All test are passing with no issues but since there were minor logic changes there might be some edge cases needed to be addressed.
Though it's more resonable to assume changes will fix bugs related to those edge cases than cause them
… sqlite casting and handling of

postgres and sqlite handle casting and JSONB formats a bit diffrently yet some previous commits
didn't took this changes into consideration when handling casting and didn't test casting properly
…y symbols

Operators will be represented internally by Symbols
User can provide their own aliases by passing `options.operatorsAliases`

Fix sequelize#7310
@mention-bot

Copy link
Copy Markdown

@yonjah, thanks for your PR! By analyzing the history of the files in this pull request, we identified @felixfbecker, @janmeier and @mickhansen to be potential reviewers.

@codecov

codecov Bot commented Sep 1, 2017

Copy link
Copy Markdown

Codecov Report

Merging #8240 into master will increase coverage by 0.03%.
The diff coverage is 98.48%.

@sushantdhiman sushantdhiman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall it looks good to me, good work. Need to check new query generator code, will do that later

Op = require('../../../../lib/operators'),
QueryGenerator = require('../../../../lib/dialects/abstract/query-generator');

require(__dirname + '/../../support');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

^ , why ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

support injects sequelize into the test context. it won't cause any issues if your running thw whole suit but if you only want to run this test file you'll need it to be loaded for the test to work

return identifier;
}

function getQueryGenerator(sequelize) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can get QueryGenerator in tests by

this.sequelize.dialect.QueryGenerator 

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All the other tests for the dialects query-generator loads them manually.
I think it also makes more sense in this context of a unit test for the abstruct query generator to get a clean and base version of query-generator since the one sequelize is using changes depending on the configuration it was loaded with and the dialect

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have two suggestions here,

  1. Keep this method here, but get QueryGenerator, directly with sequelize.dialect.QueryGenerator, no assign
  2. Move this method to support lib, so others can use it as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should I change it in all the other tests for each dialect query generator ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No just this one is enough

it('should not parse any strings as aliases operators', function() {
const QG = getQueryGenerator(this.sequelize);
expect(() => QG.whereItemQuery('$or', [{test: 5}, {test: 3}]))
.to.throw();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Specify error here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm getting an error for one of the internal packages expecting a string and getting an object.
I can specify the error but I'm not sure if it really matters for the test.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this method could be failing for any reason, we cant be ok with that it throws something. Error should be deterministic and may be specific when trying to use String alias.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can check for the specific error I'm getting.
It is hard to detect where a string alias are being used cause in this context there aren't any string aliases (otherwise the query would work) and there might even be cases where a valid query will be generated but it will not use the operator (it will be parsed as a regular field)

Comment thread test/unit/sql/add-constraint.test.js Outdated
}
}), {
default: 'ALTER TABLE [myTable] ADD CONSTRAINT [check_mycolumn_where] CHECK (([myColumn] > 50 AND [myColumn] < 100));'
default: [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why this result is now random (order of object keys ??)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not really sure why it changes or if it is caused by the code changes. 99% of the time I'm getting the original result but some time they get flipped.
In general JS asks you not to rely on the order when traversing object properties. I never actually had a case where they weren't traverse in the same order but I'm not sure if I ever had a code where it would matter.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It make testing things a bit hard, may be we should sort object keys to have single defined output for same arguments

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can look into it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@yonjah Did you figure out why this is an issue, it would be great if we can get some deterministic behavior here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

From what I could see I only get random results on node v4 and it is due to the fact that the alias method delete the previous keys -
https://github.com/yonjah/sequelize/blob/fb8188efb78eb21f91f87cbe0693220adbdf7ee9/lib/dialects/abstract/query-generator.js#L2107

This is how the old aliasing worked but I can change the alias logic to generate a new object instead of deleting properties on the old one (which might be a better solution any way but a bit more complex)

Comment thread lib/sequelize.js Outdated
isolationLevel: null,
databaseVersion: 0,
typeValidation: false,
allowLegacyOperators: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where this option is used ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry I ended up not using this option since I think it's better to just select the exact aliases you want. I'll remove it

Comment thread lib/sequelize.js Outdated
const Association = require('./associations/index');
const Validator = require('./utils/validator-extras').validator;
const _ = require('lodash');
const Operators = require('./operators');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Call it Op

Comment thread lib/sequelize.js Outdated
* Operators symbols to be used for querying data
* @see {@link Operators}
*/
Sequelize.prototype.Operators = Sequelize.Operators = Operators;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

May be expose as Sequelize.prototype.Op , cc @mickhansen @janmeier @eseliger

Comment thread lib/sequelize.js Outdated
if (this.options.operatorsAliases) {
this.dialect.QueryGenerator.setOperatorsAliases(this.options.operatorsAliases);
} else {
Utils.deprecate('Limiting available operators aliases offers improved security and will be forced in future versions. check: http://docs.sequelizejs.com/manual/tutorial/querying.html#operators');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should only be shown for FIRST time when user is actually trying to use legacy alias in code, otherwise I will get annoyed even if I am not using string based operators.

OR may be its missing else if ( this.options.allowLegacyOperators ) ????

Message should say String based operators are now deprecated. Please use Symbol based operators for better security, read more at http://docs.sequelizejs.com/manual/tutorial/querying.html#operators

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The problem is if you don't select your own aliases (or completely disable them) they are available (for backward compatibility) so most users are not even aware of the aliases available and are not using many of them (legacy ones weren't even documented) but they can still be injected even if they are not normally used.
I think showing this warning unless the user sets his own operators using the operatorsAliases is the best option to make sure users are aware of the issue.
I tried to make it clear in the documentation how to use operatorsAliases and dismiss this warning but maybe it can be made clearer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I liked the idea of default enabled flag, which will all load legacy alias, so this wont be a breaking change.

If they want better security and no deprecation warning they should just set that flag to false, Please remove user supplied operatorsAliases altogether.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's how I tried to implement it the first time.
But there are a few issues with doing it that way -

To get a fully backward supported and secure options you need two flags one for legacy aliases (which are not prefixed undocumented and probably 99% of the users we have are not using them and so hopefully would disable them) and another for the $ prefixed operators which all users are probably using and we should hope they'll switch to symbols after this is released.

since we have two flags we we'll need two warnings -

  1. Deprecation warning if legacy operators are enabled (even explicitly) since they will be removed any way in future versions
  2. Deprecation warning for string operators not being explicitly enabled (I assume those will still be available in future version but won't be enabled by default)

Now I'm ending up setting two flags to hide all deprecation warnings of a single feature.
Users are not really aware of what they are enabling or disabling.
They might only '..' alias and since it would fail enable legacy operators but then they'll be forced to also enable @>, gt and all the other legacy one.

Giving the the operatorsAliases takes control back to the user. we don't want to set those aliases for you cause you need to be aware for them. you want '..' to equal the between operator sure you can do it. want '||' to equal the or operator yea sure. But it is up to you to define those and up to you to sanitize them. Now every user knows exactly which strings will be translated into operators cause he is the one explicitly defined them.

So if my API allows client to send requests including objects using the $not alias I can enable it without having to also enable the $col alias

Comment thread lib/utils.js Outdated
}; No newline at end of file
};


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Extra lines

Comment thread docs/models-usage.md Outdated
* `count` - an integer, total number records matching the where clause
* `rows` - an array of objects, the records matching the where clause, within the limit and offset range
```js
const {like} = Sequelize.Op;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

May be update it as well, it doesn't work with our base version Node v4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please update other examples where you have used destructing

Comment thread docs/querying.md Outdated
const Op = Sequelize.Op;

//use sequelize without any operators aliases
const connection = new Sequelize(db, user, pass, { operatorsAliases: {} });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We dont even need to pass operatorAliases, right ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This specific example shows how to remove all string aliases.
so after initializing Sequelize in this way you wont be able to use even '$gt' or any other alias minimizing injection surface.
I guess I need to go over the documentation and see that this point is clear

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

May be just call it operators, setting it false or empty (can be checked with lodash) should disable legacy / string op

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This example can be updated to operatorsAliases: false, right ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yea sorry missed it when changed it back from operators

@sushantdhiman sushantdhiman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@yonjah Is it possible to divide this PR into refactor + Secure Ops ? That will make review easier and we can merge refactor one quickly

Then @sequelize/core will need to decide how to propagate this change to users properly and what API we should expose, options or maps etc

@yonjah

yonjah commented Sep 14, 2017

Copy link
Copy Markdown
Contributor Author

@sushantdhiman all the refactor commits are available in #8068
Unfortunately discussion there went stale and there was very high chance of introducing critical bugs when changing the operators without those changes

@sushantdhiman

Copy link
Copy Markdown
Contributor

Please rebase @yonjah

@yonjah

yonjah commented Sep 22, 2017

Copy link
Copy Markdown
Contributor Author

@sushantdhiman Any thing else regarding this pull request ?
Did you want me to rebase it to a single commit ?

@sushantdhiman

Copy link
Copy Markdown
Contributor

Did you want me to rebase it to a single commit ?

No need, Github has Squash and merge button

@mkaufmaner

Copy link
Copy Markdown
Contributor

I must say that I am a huge proponent of this PR. It will also bring us one step closer to having expressive operators.

@sushantdhiman sushantdhiman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍 LGTM, I have added other maintainers for final review. After their review we can release it as next minor version

@sushantdhiman
sushantdhiman requested a review from a team September 26, 2017 10:14
@mkaufmaner
mkaufmaner self-requested a review September 26, 2017 12:36

@mkaufmaner mkaufmaner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall, looks good. In the future we should refactor data types and operators in a similar manner where they are extensible by not only the dialects but the users themselves.

Comment thread test/support.js Outdated
chai = require('chai'),
expect = chai.expect;
expect = chai.expect,
AbstructQueryGenerator = require('../lib/dialects/abstract/query-generator');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Typo AbstructQueryGenerator should be AbstractQueryGenerator

Comment thread lib/sequelize.js
this.dialect = new Dialect(this);
this.dialect.QueryGenerator.typeValidation = options.typeValidation;
if (this.options.operatorsAliases === true) {
Utils.deprecate('String based operators are now deprecated. Please use Symbol based operators for better security, read more at http://docs.sequelizejs.com/manual/tutorial/querying.html#operators');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One second thought, I don't think we should remove complete support for operator aliases.

Simply put, I still want to be able to pass in a JSON object from an HTTP request into the where property.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should disable them by default in the future, but should not completely remove them, there are definitely use-cases for that, I think

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yea I agree.
I don't think there is any thought of deprecating them but users should define the ones they want to use and not have all of them available by default.

In the future it might be useful to define aliases in the query options. So for example if you have one query that you know you gonna pass right through and you want to allow for 'like' operator you can just add an alias for it that it will only be available for that specific query

@mkaufmaner
mkaufmaner self-requested a review September 26, 2017 13:37
Comment thread lib/utils.js
if (attributes) {
for (attribute in attributes) {
rawAttribute = Model.rawAttributes[attribute];
getComplexKeys(attributes).forEach(attribute => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can't we use a for .. of loop here? .foreach is so 2012

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No need, JS itself is more than 20 years old :) forEach looks clean here.

Comment thread test/unit/sql/where.test.js Outdated
user_id: 2
}
},
shared: 1,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

trailing comma can be removed :)

@eseliger
eseliger merged commit ccb99da into sequelize:master Sep 30, 2017
@sushantdhiman

Copy link
Copy Markdown
Contributor

Wanted to have a review by @mickhansen / @janmeier for deprecating API but I think its ok.

@mickhansen

Copy link
Copy Markdown
Contributor

@sushantdhiman I like it, only thing i would change is a bit of interface/documentation, Op with uppercase is a bit annoying so maybe import {operator /* as op /*} from 'sequelize'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hash injection (security)

6 participants