Secure operators - #8240
Conversation
…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
…plain js object values for JSON column
…y symbols Operators will be represented internally by Symbols User can provide their own aliases by passing `options.operatorsAliases` Fix sequelize#7310
|
@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. |
…uery might have mulitple valid options
sushantdhiman
left a comment
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
You can get QueryGenerator in tests by
this.sequelize.dialect.QueryGenerator There was a problem hiding this comment.
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
There was a problem hiding this comment.
I have two suggestions here,
- Keep this method here, but get QueryGenerator, directly with
sequelize.dialect.QueryGenerator, no assign - Move this method to support lib, so others can use it as well
There was a problem hiding this comment.
Should I change it in all the other tests for each dialect query generator ?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
| } | ||
| }), { | ||
| default: 'ALTER TABLE [myTable] ADD CONSTRAINT [check_mycolumn_where] CHECK (([myColumn] > 50 AND [myColumn] < 100));' | ||
| default: [ |
There was a problem hiding this comment.
Why this result is now random (order of object keys ??)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
It make testing things a bit hard, may be we should sort object keys to have single defined output for same arguments
There was a problem hiding this comment.
@yonjah Did you figure out why this is an issue, it would be great if we can get some deterministic behavior here
There was a problem hiding this comment.
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)
| isolationLevel: null, | ||
| databaseVersion: 0, | ||
| typeValidation: false, | ||
| allowLegacyOperators: true, |
There was a problem hiding this comment.
Where this option is used ?
There was a problem hiding this comment.
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
| const Association = require('./associations/index'); | ||
| const Validator = require('./utils/validator-extras').validator; | ||
| const _ = require('lodash'); | ||
| const Operators = require('./operators'); |
| * Operators symbols to be used for querying data | ||
| * @see {@link Operators} | ||
| */ | ||
| Sequelize.prototype.Operators = Sequelize.Operators = Operators; |
There was a problem hiding this comment.
May be expose as Sequelize.prototype.Op , cc @mickhansen @janmeier @eseliger
| 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'); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 -
- Deprecation warning if legacy operators are enabled (even explicitly) since they will be removed any way in future versions
- 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
| }; No newline at end of file | ||
| }; | ||
|
|
||
|
|
… operatorsAliases option
…d to test support
… use invalid alias
73edc59 to
84efcbd
Compare
| * `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; |
There was a problem hiding this comment.
May be update it as well, it doesn't work with our base version Node v4
There was a problem hiding this comment.
Please update other examples where you have used destructing
| const Op = Sequelize.Op; | ||
|
|
||
| //use sequelize without any operators aliases | ||
| const connection = new Sequelize(db, user, pass, { operatorsAliases: {} }); |
There was a problem hiding this comment.
We dont even need to pass operatorAliases, right ?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
May be just call it operators, setting it false or empty (can be checked with lodash) should disable legacy / string op
There was a problem hiding this comment.
This example can be updated to operatorsAliases: false, right ?
There was a problem hiding this comment.
Yea sorry missed it when changed it back from operators
sushantdhiman
left a comment
There was a problem hiding this comment.
@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
|
@sushantdhiman all the refactor commits are available in #8068 |
|
Please rebase @yonjah |
|
@sushantdhiman Any thing else regarding this pull request ? |
No need, Github has |
|
I must say that I am a huge proponent of this PR. It will also bring us one step closer to having expressive operators. |
36a5b8b to
2606d09
Compare
sushantdhiman
left a comment
There was a problem hiding this comment.
👍 LGTM, I have added other maintainers for final review. After their review we can release it as next minor version
mkaufmaner
left a comment
There was a problem hiding this comment.
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.
| chai = require('chai'), | ||
| expect = chai.expect; | ||
| expect = chai.expect, | ||
| AbstructQueryGenerator = require('../lib/dialects/abstract/query-generator'); |
There was a problem hiding this comment.
Typo AbstructQueryGenerator should be AbstractQueryGenerator
| 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'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
| if (attributes) { | ||
| for (attribute in attributes) { | ||
| rawAttribute = Model.rawAttributes[attribute]; | ||
| getComplexKeys(attributes).forEach(attribute => { |
There was a problem hiding this comment.
can't we use a for .. of loop here? .foreach is so 2012
There was a problem hiding this comment.
No need, JS itself is more than 20 years old :) forEach looks clean here.
| user_id: 2 | ||
| } | ||
| }, | ||
| shared: 1, |
There was a problem hiding this comment.
trailing comma can be removed :)
|
Wanted to have a review by @mickhansen / @janmeier for deprecating API but I think its ok. |
|
@sushantdhiman I like it, only thing i would change is a bit of interface/documentation, |
Pull Request check-list
Please make sure to review and check all of these items:
npm run testornpm run test-DIALECTpass with this change (including linting)?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