Our response to RCE Security Advisory
We've just published a critical security advisory relating to a Remote Code Execution vulnerability in Dynamic JSON/TOML/YAML badges: https://github.com/badges/shields/security/advisories/GHSA-rxvx-x284-4445 Thanks to @nickcopi for his help with this.
If you self-host your own instance of Shields you should upgrade to server-2024-09-25 or later as soon as possible to protect your instance.
This is primarily a concern for self-hosting users. However this does also have a couple of knock-on implications for some users of shields.io itself.
1. Users who have authorized the Shields.io GitHub OAuth app
While we have taken steps to close this vulnerability quickly after becoming aware of it, this attack vector has existed in our application for some time. We aren't aware of it having been actively exploited on shields.io. We also can't prove that it has not been exploited.
We don't log or track our users, so a breach offers a very limited attack surface against end users of shields.io. This is by design. One of the (few) information assets shields.io does hold is our GitHub token pool. This allows users to share a token with us by authorizing our OAuth app. Doing this gives us access to a token with read-only access to public data which we can use to increase our rate limit when making calls to the GitHub API.
The tokens we hold are not of high value to an attacker because they have read-only access to public data, but we can't say for sure they haven't been exfiltrated. If you've donated a token in the past and want to revoke it, you can revoke the Shields.io OAuth app at https://github.com/settings/applications which will de-activate the token you have shared with us.
2. Users of Dynamic JSON/TOML/YAML badges
Up until now, we have been using https://github.com/dchester/jsonpath as our library querying documents using JSONPath expressions. @nickcopi responsibly reported to us how a prototype pollution vulnerability in this library could be exploited to construct a JSONPath expression allowing an attacker to perform remote code execution. This vulnerability was reported on the package's issue tracker but not flagged by security scanning tools. It seems extremely unlikely that this will be fixed in the upstream package despite being widely used. It also seems unlikely this package will receive any further maintenance in future, even in response to critical security issues. In order to resolve this issue, we needed to switch to a different JSONPath library. We've decided to switch https://github.com/JSONPath-Plus/JSONPath using the eval: false
option to disable script expressions.
This is an important security improvement and we have to make a change to protect the security of shields.io and users hosting their own instance of the application. However, this does come with some tradeoffs from a backwards-compatibility perspective.
Using eval: false
Using JSONPath-Plus with eval: false
does disable some query syntax which relies on evaluating javascript expressions.
For example, it would previously have been possible to use a JSONPath query like $..keywords[(@.length-1)]
against the document https://github.com/badges/shields/raw/master/package.json to select the last element from the keywords array https://github.com/badges/shields/blob/e237e40ab88b8ad4808caad4f3dae653822aa79a/package.json#L6-L12
This is now not a supported query.
In this particular case, you could rewrite that query to $..keywords[-1:]
and obtain the same result, but that may not be possible in all cases. We do realise that this removes some functionality that previously worked but closing this remote code execution vulnerability is the top priority, especially since there will be workarounds in many cases.
Implementation Quirks
Historically, every JSONPath implementation has had its own idiosyncrasies. While most simple and common queries will behave the same way across different implementations, switching to another library will mean that some subset of queries may not work or produce different results.
One interesting thing that has happened in the world of JSONPath lately is RFC 9535 https://www.rfc-editor.org/rfc/rfc9535 which is an attempt to standardise JSONPath. As part of this mitigation, we did look at whether it would be possible to migrate to something that is RFC9535-compliant. However it is our assessment that the JavaScript community does not yet have a sufficiently mature/supported RFC9535-compliant JSONPath implementation. This means we are switching from one quirky implementation to another implementation with different quirks.
Again, this represents an unfortunate break in backwards-compatibility. However, it was necessary to prioritise closing off this remote code execution vulnerability over backwards-compatibility.
Although we can not provide a precise migration guide, here is a table of query types where https://github.com/dchester/jsonpath and https://github.com/JSONPath-Plus/JSONPath are known to diverge from the consensus implementation. This is sourced from the excellent https://cburgmer.github.io/json-path-comparison/ While this is a long list, many of these inputs represent edge cases or pathological inputs rather than common usage.
Table
Query Type | Example Query |
---|---|
Array slice with large number for end and negative step | $[2:-113667776004:-1] |
Array slice with large number for start end negative step | $[113667776004:2:-1] |
Array slice with negative step | $[3:0:-2] |
Array slice with negative step on partially overlapping array | $[7:3:-1] |
Array slice with negative step only | $[::-2] |
Array slice with open end and negative step | $[3::-1] |
Array slice with open start and negative step | $[:2:-1] |
Array slice with range of 0 | $[0:0] |
Array slice with step 0 | $[0:3:0] |
Array slice with step and leading zeros | $[010:024:010] |
Bracket notation with empty path | $[] |
Bracket notation with number on object | $[0] |
Bracket notation with number on string | $[0] |
Bracket notation with number -1 | $[-1] |
Bracket notation with quoted array slice literal | $[':'] |
Bracket notation with quoted closing bracket literal | $[']'] |
Bracket notation with quoted current object literal | $['@'] |
Bracket notation with quoted escaped backslash | $['\'] |
Bracket notation with quoted escaped single quote | $['''] |
Bracket notation with quoted root literal | $['$'] |
Bracket notation with quoted special characters combined | $[':@."$,*'\'] |
Bracket notation with quoted string and unescaped single quote | $['single'quote'] |
Bracket notation with quoted union literal | $[','] |
Bracket notation with quoted wildcard literal ? | $['*'] |
Bracket notation with quoted wildcard literal on object without key | $['*'] |
Bracket notation with spaces | $[ 'a' ] |
Bracket notation with two literals separated by dot | $['two'.'some'] |
Bracket notation with two literals separated by dot without quotes | $[two.some] |
Bracket notation without quotes | $[key] |
Current with dot notation | @.a |
Dot bracket notation | $.['key'] |
Dot bracket notation with double quotes | $.["key"] |
Dot bracket notation without quotes | $.[key] |
Dot notation after recursive descent with extra dot ? | $...key |
Dot notation after union with keys | $['one','three'].key |
Dot notation with dash | $.key-dash |
Dot notation with double quotes | $."key" |
Dot notation with double quotes after recursive descent ? | $.."key" |
Dot notation with empty path | $. |
Dot notation with key named length on array | $.length |
Dot notation with key root literal | $.$ |
Dot notation with non ASCII key | $.?? |
Dot notation with number | $.2 |
Dot notation with number -1 | $.-1 |
Dot notation with single quotes | $.'key' |
Dot notation with single quotes after recursive descent ? | $..'key' |
Dot notation with single quotes and dot | $.'some.key' |
Dot notation with space padded key | $. a |
Dot notation with wildcard after recursive descent on scalar ? | $..* |
Dot notation without dot | $a |
Dot notation without root | .key |
Dot notation without root and dot | key |
Empty | n/a |
Filter expression on object | $[?(@.key)] |
Filter expression after dot notation with wildcard after recursive descent ? | $..*[?(@.id>2)] |
Filter expression after recursive descent ? | $..[?(@.id==2)] |
Filter expression with addition | $[?(@.key+50==100)] |
Filter expression with boolean and operator and value false | $[?(@.key>0 && false)] |
Filter expression with boolean and operator and value true | $[?(@.key>0 && true)] |
Filter expression with boolean or operator and value false | $[?(@.key>0 || false)] |
Filter expression with boolean or operator and value true | $[?(@.key>0 || true)] |
Filter expression with bracket notation with -1 | $[?(@[-1]==2)] |
Filter expression with bracket notation with number on object | $[?(@[1]=='b')] |
Filter expression with current object | $[?(@)] |
Filter expression with different ungrouped operators | $[?(@.a && @.b || @.c)] |
Filter expression with division | $[?(@.key/10==5)] |
Filter expression with dot notation with dash | $[?(@.key-dash == 'value')] |
Filter expression with dot notation with number | $[?(@.2 == 'second')] |
Filter expression with dot notation with number on array | $[?(@.2 == 'third')] |
Filter expression with empty expression | $[?()] |
Filter expression with equals | $[?(@.key==42)] |
Filter expression with equals on array of numbers | $[?(@==42)] |
Filter expression with equals on object | $[?(@.key==42)] |
Filter expression with equals array | $[?(@.d==["v1","v2"])] |
Filter expression with equals array for array slice with range 1 | $[?(@[0:1]==[1])] |
Filter expression with equals array for dot notation with star | $[?(@.*==[1,2])] |
Filter expression with equals array or equals true | $[?(@.d==["v1","v2"] || (@.d == true))] |
Filter expression with equals array with single quotes | $[?(@.d==['v1','v2'])] |
Filter expression with equals boolean expression value | $[?((@.key<44)==false)] |
Filter expression with equals false | $[?(@.key==false)] |
Filter expression with equals null | $[?(@.key==null)] |
Filter expression with equals number for array slice with range 1 | $[?(@[0:1]==1)] |
Filter expression with equals number for bracket notation with star | $[?(@[*]==2)] |
Filter expression with equals number for dot notation with star | $[?(@.*==2)] |
Filter expression with equals number with fraction | $[?(@.key==-0.123e2)] |
Filter expression with equals number with leading zeros | $[?(@.key==010)] |
Filter expression with equals object | $[?(@.d=={"k":"v"})] |
Filter expression with equals string | $[?(@.key=="value")] |
Filter expression with equals string with unicode character escape | $[?(@.key=="Mot\u00f6rhead")] |
Filter expression with equals true | $[?(@.key==true)] |
Filter expression with equals with path and path | $[?(@.key1==@.key2)] |
Filter expression with equals with root reference | $.items[?(@.key==$.value)] |
Filter expression with greater than | $[?(@.key>42)] |
Filter expression with greater than or equal | $[?(@.key>=42)] |
Filter expression with in array of values | $[?(@.d in [2, 3])] |
Filter expression with in current object | $[?(2 in @.d)] |
Filter expression with length free function | $[?(length(@) == 4)] |
Filter expression with length function | $[?(@.length() == 4)] |
Filter expression with length property | $[?(@.length == 4)] |
Filter expression with less than | $[?(@.key<42)] |
Filter expression with less than or equal | $[?(@.key<=42)] |
Filter expression with local dot key and null in data | $[?(@.key='value')] |
Filter expression with multiplication | $[?(@.key*2==100)] |
Filter expression with negation and equals | $[?(!(@.key==42))] |
Filter expression with negation and equals array or equals true | $[?(!(@.d==["v1","v2"]) || (@.d == true))] |
Filter expression with negation and less than | $[?(!(@.key<42))] |
Filter expression with negation and without value | $[?(!@.key)] |
Filter expression with non singular existence test | $[?(@.a.*)] |
Filter expression with not equals | $[?(@.key!=42)] |
Filter expression with not equals array or equals true | $[?((@.d!=["v1","v2"]) || (@.d == true))] |
Filter expression with parent axis operator | $[*].bookmarks[?(@.page == 45)]^^^ |
Filter expression with regular expression | $[?(@.name=~/hello.*/)] |
Filter expression with regular expression from member | $[?(@.name=~/@.pattern/)] |
Filter expression with set wise comparison to scalar | $[?(@[*]>=4)] |
Filter expression with set wise comparison to set | $.x[?(@[]>=$.y[])] |
Filter expression with single equal | $[?(@.key=42)] |
Filter expression with subfilter | $[?(@.a[?(@.price>10)])] |
Filter expression with subpaths deeply nested | $[?(@.a.b.c==3)] |
Filter expression with subtraction | $[?(@.key-50==-100)] |
Filter expression with triple equal | $[?(@.key===42)] |
Filter expression with value | $[?(@.key)] |
Filter expression with value after recursive descent ? | $..[?(@.id)] |
Filter expression with value false | $[?(false)] |
Filter expression with value from recursive descent | $[?(@..child)] |
Filter expression with value null | $[?(null)] |
Filter expression with value true | $[?(true)] |
Filter expression without parens | $[?@.key==42] |
Filter expression without value | $[?(@.key)] |
Function sum | $.data.sum() |
Parens notation | $(key,more) |
Recursive descent ? | $.. |
Recursive descent after dot notation ? | $.key.. |
Root on scalar | $ |
Root on scalar false | $ |
Root on scalar true | $ |
Script expression | $[(@.length-1)] |
Union with duplication from array | $[0,0] |
Union with duplication from object | $['a','a'] |
Union with filter | $[?(@.key<3),?(@.key>6)] |
Union with keys | $['key','another'] |
Union with keys on object without key | $['missing','key'] |
Union with keys after array slice | $[:]['c','d'] |
Union with keys after bracket notation | $[0]['c','d'] |
Union with keys after dot notation with wildcard | $.*['c','d'] |
Union with keys after recursive descent ? | $..['c','d'] |
Union with repeated matches after dot notation with wildcard | $.*[0,:5] |
Union with slice and number | $[1:3,4] |
Union with spaces | $[ 0 , 1 ] |
Union with wildcard and number | $[*,1] |