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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic tests for prototype safety #414

Closed
wants to merge 4 commits into from

Conversation

ChALkeR
Copy link
Member

@ChALkeR ChALkeR commented Jul 8, 2020

Language-specific implementation details shouldn't affect property existence checks.
Validators should not traverse JS prototype chain.

If a validator supports applying default values from the schema, it should pass with that both enabled or disabled.

Testing that might detect significant issues in e.g. JS implementations (although I attempted to target Lua as well, but that's completely untested against any Lua impl).

This test is generated by a generator of test 5 blocks, here: https://gist.github.com/ChALkeR/38c2753f9420feccbaac036b83bd51c0
Should I include the generator somehow, perhaps? If so, where should I place it?

Language-specific implementation details shouldn't affect property
existence checks.

Validators should not traverse JS prototype chain or assign to it.

If a validator supports applying default values from the schema, it
should pass with that both enabled or disabled.

Testing that might detect significant issues in e.g. JS implementations.
@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 8, 2020

Included the generator in test-generator dir, as discussed in Slack. Self-contained.

[chalker@xps JSON-Schema-Test-Suite]$ ./test-generator/prototype-safe.js | shasum
d531471c5e2ca0cafc15da22018e65a972bcf40b  -
[chalker@xps JSON-Schema-Test-Suite]$ find . -name prototype-safe.json | xargs shasum
d531471c5e2ca0cafc15da22018e65a972bcf40b  ./tests/draft2019-09/prototype-safe.json
d531471c5e2ca0cafc15da22018e65a972bcf40b  ./tests/draft7/prototype-safe.json
d531471c5e2ca0cafc15da22018e65a972bcf40b  ./tests/draft4/prototype-safe.json
d531471c5e2ca0cafc15da22018e65a972bcf40b  ./tests/draft6/prototype-safe.json

@awwright
Copy link
Member

Do we really need almost a thousand lines of tests (per directory) to test for this?

It seems reasonable to me to check for common bugs, even language-specific ones, but could we make do with a single test in properties.json?

@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 14, 2020

@awwright No, these test for a series of failure points, not just a single bug.

I can remove the ones that are targeting Lua though, as those weren't checked against any actual Lua validator and I'm not sure if they are useful.

The js ones found different issues in several js implementations though, I would prefer not to remove them.

@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 14, 2020

@awwright I removed Lua tests. This is now ~600 lines.

Until they are checked against any actual Lua impl and it's confirmed that they are useful.
@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 14, 2020

@awwright This is a demonstration of how badly js impls fail on this: ChALkeR/json-schema-benchmark@0f1b068
One fails so badly that it affects other validators though global side-effects. In default configuration.

This was also a security vulnerability is ajv leading to code execution: https://github.com/ajv-validator/ajv/releases/tag/v6.12.3
I delayed this PR specifically until ajv was fixed enough so this wouldn't be a vulnerability anymore.

I know only two three js-based validators that pass -- schemasafe, hyperjump, and jassi. Of those, hyperjump fixed the issues because of the tests in this pr.

@awwright
Copy link
Member

It seems like most of these tests are testing the same thing. For example, the outcome isn't going to change because you used an array instead of an object.

Is there any case where one of the tests failing is not predictive of the other tests failing? I.e. is it feasible to reduce this to a single schema being tested, with a positive and negative case, in properties.json?

Was the code execution because of how ajv compiles to ECMAScript code? I poked around with this idea some time ago and concluded since you can't store functions in JSON, it's pretty difficult do do anything malicious.

@awwright
Copy link
Member

I mean, to reiterate my earlier point, I think it's OK to test for a common platform-specific or language-specific bug; and if you're adding a second test, it's either testing for something completely different, or it's testing for something that the first one is likely to pick up (in which case, it's not really testing for a "common" bug).

@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 15, 2020

Was the code execution because of how ajv compiles to ECMAScript code? I poked around with this idea some time ago and concluded since you can't store functions in JSON, it's pretty difficult do do anything malicious.

There were multiple issues in ajv each separately leading to code execution. One of them was directly related to this + useDefaults option -- that was enough to gain RCE.

Re: duplication -- if you check ChALkeR/json-schema-benchmark@0f1b068, multiple impls fail different subsets of these tests.
There might be e.g. checks specifically for certain property keys (like __proto__) -- that is not enough to make this work and hence other tests shouldn't be excluded.

I don't think this could be reduced to a single one -- that might slip some kind of errors through, which is might be dangerous given the security nature of this.

@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 15, 2020

@awwright It would be possible to merge this into one huge schema with subschemas for properties and test everything against it (in properties), but I don't see how would that be better than the current approach.

@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 15, 2020

Here, I split the commit linked above in four parts to make it more readable:
ChALkeR/json-schema-benchmark@e94479f...e991828

  1. ChALkeR/json-schema-benchmark@032715f -- immediate action of this test
  2. ChALkeR/json-schema-benchmark@84d43a6 -- first-order side effects
  3. ChALkeR/json-schema-benchmark@bb4f07a -- one validator breaks other validators via second-order effects
  4. ChALkeR/json-schema-benchmark@e991828 -- second-order side effects

There, it's easy to see in the first commit that different validators fail different subsets of these tests.

@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 15, 2020

@awwright Specifically, djv fails only the length test. It can't be removed.
For comparison, ajv doesn't fail length test but fails others.

I don't recommend removing any tests from here based on guesses how they should automatically pass if others pass.

@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 15, 2020

Here is a list of failures minus one validator.

Note that this is condensed, and some validators different tests inside them -- this is a list of schemas which caused failures.
Also this is just the Does not see block, Default value block is different (and causes different failures).

Expand
ajvconstructoras number
ajvconstructoras object
ajvconstructorvia required
ajv__proto__as number
ajv__proto__as object
ajv__proto__via required
ajvtoStringas number
ajvtoStringas object
ajvtoStringvia required
@cfworker/json-schemaconstructoras number
@cfworker/json-schemaconstructoras object
@cfworker/json-schemaconstructorvia required
@cfworker/json-schema__proto__as number
@cfworker/json-schema__proto__via required
@cfworker/json-schematoStringas number
@cfworker/json-schematoStringas object
@cfworker/json-schematoStringvia required
djvlengthas object
is-my-json-validconstructoras number
is-my-json-validconstructoras object
is-my-json-validconstructorvia required
is-my-json-validlengthas object
is-my-json-valid__proto__as number
is-my-json-valid__proto__as object
is-my-json-valid__proto__via required
is-my-json-validtoStringas number
is-my-json-validtoStringas object
is-my-json-validtoStringvia required
jjvconstructorvia required
jjvlengthvia required
jjv__proto__via required
jjvtoStringvia required
jjvxvia required
jsckconstructorvia required
jsck__proto__via required
jscktoStringvia required
jsenconstructoras number
jsenconstructoras object
jsenconstructorvia required
jsen__proto__as number
jsen__proto__via required
jsentoStringas number
jsentoStringas object
jsentoStringvia required
json-modelconstructoras number
json-modelconstructoras object
json-modelconstructorvia required
json-model__proto__as number
json-model__proto__as object
json-model__proto__via required
json-modeltoStringas number
json-modeltoStringas object
json-modeltoStringvia required
json-schema-libraryconstructoras number
json-schema-libraryconstructoras object
json-schema-libraryconstructorvia required
json-schema-library__proto__as number
json-schema-library__proto__via required
json-schema-librarytoStringas number
json-schema-librarytoStringas object
json-schema-librarytoStringvia required
jsonschemaconstructorvia required
jsonschema__proto__via required
jsonschematoStringvia required
json-schema-validator-generatorconstructoras number
json-schema-validator-generatorconstructoras object
json-schema-validator-generatorconstructorvia required
json-schema-validator-generatorlengthvia required
json-schema-validator-generator__proto__as number
json-schema-validator-generator__proto__via required
json-schema-validator-generatortoStringas number
json-schema-validator-generatortoStringas object
json-schema-validator-generatortoStringvia required
json-schema-validator-generatorxvia required
JSVconstructoras number
JSVconstructoras object
JSVconstructorvia required
JSVlengthvia required
JSV__proto__as number
JSV__proto__as object
JSV__proto__via required
JSVtoStringas number
JSVtoStringas object
JSVtoStringvia required
JSVxvia required
request-validatorconstructoras number
request-validatorconstructoras object
request-validatorconstructorvia required
request-validator__proto__as number
request-validator__proto__as object
request-validator__proto__via required
request-validatortoStringas number
request-validatortoStringas object
request-validatortoStringvia required
revalidatorconstructoras number
revalidatorconstructoras object
revalidatorconstructorvia required
revalidatorlengthas object
revalidatorlengthvia required
revalidator__proto__as number
revalidator__proto__as object
revalidator__proto__via required
revalidatortoStringas number
revalidatortoStringas object
revalidatortoStringvia required
revalidatorxvia required
schemasaurusconstructoras number
schemasaurusconstructoras object
schemasauruslengthas object
schemasaurus__proto__as number
schemasaurus__proto__as object
schemasaurustoStringas number
schemasaurustoStringas object
skeemasconstructoras number
skeemasconstructoras object
skeemasconstructorvia required
skeemas__proto__as number
skeemas__proto__via required
skeemastoStringas number
skeemastoStringas object
skeemastoStringvia required
themisconstructorvia required
themislengthvia required
themis__proto__via required
themistoStringvia required
themisxvia required
tv4constructorvia required
tv4__proto__via required
tv4toStringvia required
z-schemaconstructorvia required
z-schema__proto__as number
z-schema__proto__as object
z-schema__proto__via required
z-schematoStringvia required

@awwright
Copy link
Member

What does "via required" mean here?

@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 15, 2020

@awwright: It's a part of the test description

Generator:

Test json file:

Seems that descriptions in Default value blocks are off, I'll fix that.
But that doesn't affect the list above, as that's from the Does not see blocks.
Upd: ah, not, it affects those. Will fix the table in a moment.
Upd2: fixed, thanks! List in #414 (comment) updated.

@jdesrosiers
Copy link
Member

Do we really need almost a thousand lines of tests (per directory) to test for this?

This was my impression as well. When I ran these tests on my implementation, I had 56 errors. There were only three places in my code that needed changes. 56 test failures for 3 bugs indicates that there is a lot of redundancy in these tests.

However, just because these tests are redundant the way my implementation is designed, doesn't necessarily mean they are going to be redundant for another implementation that makes different design decisions. So, it's hard to say what the right balance is.

Although I'm grateful that these tests found bugs in my implementation, I don't think the official test suite should be this exhaustive for language specific tests. Every implementation should have their own tests in addition to the official suite that tests their API and the quirks of the language they are using.

My suggestion would be to add a few tests to the main suite that should cover most prototype safety issues. Three tests would have been sufficient for me and I didn't consider prototype safety at all when implementing, so I think 3-10 tests should be sufficient to catch all issues in all but the most poorly designed code. The full exhaustive set of tests (or at least the generation script) can be included as an optional suite for js implementations who want to be really really sure they're not missing anything.

@ChALkeR
Copy link
Member Author

ChALkeR commented Jul 15, 2020

@jdesrosiers This now has 30 tests. For comparison, #385 introduces 172 tests.

Different implementations have different bugs, as can be seen in the list above.
Some fail only one or three tests. It would be hard to reduce this and something might get missed.

It's testing not one failure point but many, and as you mentioned, that was 3 bugs, not one, in a single impl.

@awwright
Copy link
Member

@jdesrosiers Would you be able to show me the fix that you implemented?

@jdesrosiers
Copy link
Member

@awwright

For the required keyword, the fix was simply to use hasOwnProperty instead of in. The other two places were using Object.create(null) instead of {} to create new objects. This creates an object with a null prototype which fixes all the issues with properties named __proto__.

@jdesrosiers
Copy link
Member

jdesrosiers commented Jul 21, 2020

@ChALkeR

This now has 30 tests.

This isn't 30 tests. There are 30 subjects that each have 12 tests.

For comparison, #385 introduces 172 tests.

Yep, I find that problematic as well.

It would be hard to reduce this and something might get missed.

You have all that data on which implementations are failing which tests. You should be able to use that data to determine which are the most valuable tests. Like I said, I'm not worried about catching every possible language specific error in the main test suite. I really don't think it should have hundreds of tests that only apply to JavaScript. Having a few of the most valuable tests is fine. Having the exhaustive set of tests available in a separate area (similar to "optional" tests) is fine too.

That's my opinion, but I don't care enough argue about it. Whatever you all decide to do with this is fine with me.

@awwright
Copy link
Member

So it sounds like we can pick up on the vast majority of problems with 2-3 tests for ECMAScript and Lua each, then.

@Julian
Copy link
Member

Julian commented Jul 29, 2020

Perhaps there's an "easy" compromise to be made here -- think we just need to make a call.

Specifically:

  • Add the 2-3 or however many tests that we think cover the majority of the problems directly to the suite
  • Still merge and ship the script to generate the full spectrum of cases, such that "at-risk" implementations can run the generator and then run all of its outputted cases

Does that sound agreeable? Probably relevant for #385 as well if we make it "policy" for how we handle large numbers of generated duplicated tests.

@Julian
Copy link
Member

Julian commented Aug 7, 2020

Going to take the silence as "yes" :D -- @ChALkeR can you pick a smallish subset of these (e.g. 3 that cover 80% of the possible issues), submit those to the regular suite, and then obviously keep the generating script, but probably we need to document how to run it (and who should run it) in a sentence or two in the README?

@Julian
Copy link
Member

Julian commented Jun 22, 2022

Any chance anyone who benefited from these is willing to do ^ (pick 3-5)? Otherwise will likely pick whatever few using my less experienced Javascript eye so we can close & merge.

@Julian Julian added the waiting for author A pull request which is waiting for an update from its author. label Jun 22, 2022
Julian added a commit that referenced this pull request Aug 7, 2022
This extracts tests from #414, originally written in a slightly more
granular manner by @ChALkeR, but here combines and pares them down so we
only add ~10 rather than hundreds. Hopefully these should at least point
implementers at the issue. If any real-world occurrences of bugs are
uncovered that aren't covered by these, please raise a follow-up issue!

Interested implementers may also reference the PR if they wish to run a
fuller set of them.

Co-authored by: Nikita Skovoroda <chalkerx@gmail.com>
@Julian
Copy link
Member

Julian commented Aug 7, 2022

I've extracted a subset of these (10-15) and pushed them as 597b1fb (across all the drafts). Roughly I pared down to one schema per property, and combined them into one test case. If there's further improvements needed please speak up in a future issue or PR, otherwise closing this! Thanks for submitting them.

@Julian Julian closed this Aug 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
waiting for author A pull request which is waiting for an update from its author.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants