GitHub Security Lab CTF 3 write-up: XSS-unsafe jQuery plugins
Challenge
The challenge was to write a CodeQL query which can detect a dangerous XSS vector pattern: $(<css-selector>)
in bootstrap plugins for which the <css-selector>
can be reached from one of the plugin options. The reason why this is dangerous is because clients of the plugin might not be aware of this vector and might pass unsanitized user-input as an option leading to XSS.
Steps
The challenge was broken into steps which made our query progressively better.
Step 0: Finding the arguments to potentially unsafe jQuery function calls
This step is about understanding some basic in-built models and finding valid xss sinks.
jquery()
: a Data Flow node corresponding to $. From what I understood it is useful if we want to catch not only direct $(‘css-selector’) invocations but also patterns likef('css-selector')
where f may have been assigned the value$
JQuery::MethodCall::interpretsArgumentAsHtml
: Even better as it this can help us find other possible function calls which can be dangerous because they might be interpreting the argument as Html.
Predicate definition:
So it’s not only $(<css-selector>)
that is a valid sink, but also functions like something.after(arg)
or something.append(arg)
if arg happens to be a user-controlled css-selector. Query for a sink:
which found 133 results as the question expected.
Step 1: Finding jQuery plugins and their options
Plugin definitions can be simple like: $.fn.copyText = function() { ... }
which can be detected by query like:
Note that $.fn.something = something_else is considered to be a prperty read for “fn” and a property write for “x”.
There might also be plugin definitions which are indirect assignments to $.fn.
which can be if caught data-flow is taken into account with a query like:
which finds 13 results as expceted.
Note: The data-flow happens to the RHS of the property write (
jqdotfndotx.getRhs()
) and not the assignment itself (jqdotfndotx
).
After finding the plugin definitions, the next step is to find plugin options. The plugin option object can be found as one of the arguments in the plugin definition. The query below selects the last parameter (as in the challenge description) or any parameter with “option” in its name:
Direct property reads of the options object gives 0 results because it gets modified before a property is read. Like this for example:
In order to account for this the TaintTracking needs to be used. This is done by extending the TaintTracking:Configuration
class and defining the isSource
and isSink
predicates for the taint flow. Valid sources are the options object and sinks are the possibly vulnerable function calls which might interpret their arguments as html (Step 0):
Because isSource
and isSink
are predicates (with boolean return values), the queries from previous steps needed to be restructured within exists()
clauses. Comparing the results of running this query on the patched and unpatched versions of bootstrap codebase:
File | Line | Classification | Explanation |
---|---|---|---|
js/affix.js | 19 | False Positive | The ternary conditional before the vulnerable sink ensures that malicious user input doesn’t reach it. So it is not vulnerable anymore. |
js/scrollspy.js | 113 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code |
js/scrollspy.js | 127 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code |
File | Line | Classification | Explanation |
---|---|---|---|
js/affix.js | 19 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/collapse.js | 140 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/scrollspy.js | 113 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/scrollspy.js | 127 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/tooltip.js | 54 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/tooltip.js | 207 | False Negative | My query does not handle taint propagated through (pseudo) class fields |
Step 3: Triaging the query results
The query finds variants that were not patched: at js/scrollspy.js lines 113,127
. An exploit proof-of-concept would look like:
The query also generated a false-negative at: js/tooltip:207. That happened because the taint tracking library does not take taint propagated through (pseudo) class fields into account by default. This essentially means that the following is not considered a taint step by default:
This can be accounted for by overriding another predicate isAdditionalTaintStep
for the Configuration class as follows:
Note: Later going through the documentation, I came across the library function
DataFlow::localFieldStep
which does this.
Adding this to the original query, the comparison between the patched and unpatched version becomes:
Unpatched:
File | Line | Classification | Explanation |
---|---|---|---|
js/affix.js | 19 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/collapse.js | 140 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/scrollspy.js | 113 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/scrollspy.js | 127 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/tooltip.js | 54 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/tooltip.js | 207 | True Positive | My query does not handle taint propagated through (pseudo) class fields |
js/tooltip.js | 432 | False Positive | This is an intended sink for an HTML string with a default value that indicates the same |
File | Line | Classification | Explanation |
---|---|---|---|
js/affix.js | 19 | False Positive | The ternary conditional before the vulnerable sink ensures that malicious user input doesn’t reach it. So it is not vulnerable anymore. |
js/scrollspy.js | 113 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/scrollspy.js | 127 | True Positive | Intended usage is for a css selector but can be abused to execute JavaScript code. $(document).find should be used instead. |
js/tooltip.js | 432 | False Positive | This is an intended sink for an HTML string with a default value that indicates the same |
Step 4: Inspiration for further improvements
- While using JQuery::MethodCall::interpretsArgumentAsHtml: functions like .append(…) are considered valid sinks by the query even though it does not interpret its argument as a css selector. A more precise query should consider only those methodcalls as valid sinks for which the argument can be ambiguously interpretted as both html and css:
Full query: https://lgtm.com/query/8326532560602353549/
- Another thing that the query might miss is deliberate XSS sinks for which the programmer intends dynamically constructed HTML to be inserted there. Taking this into account would remove the false positive found earlier at js/tooltip:432 because the default value of
template
is an HTML string and not a CSS selector:
Full query here: https://lgtm.com/query/4286196275218248538/
-
The query would also probably generate more false-positives if it considers sanitized arguments as valid sinks too. These can be detected by extending the TaintTracking::SanitizerGuardNode and overriding the isSanitizerGuard predicate of our configuration class accordingly. Query at: https://lgtm.com/query/2562618391260740405/.
-
Another improvement to the query would be to have the plugin and option name in the alert message.
The final query looks like https://lgtm.com/query/5711000722013907995/
Actual solution
Answers & Feedback - GitHub Security Lab CTF 3 has the ideal answers and feedback to all the CTF submissions. My query was definitely not ideal but I think it was good enough for a start. I won first place and a nintendo switch which is awesome too :)
Big thanks to Github Security Lab for creating this CTF.