jekyll-mathjax-csp: Uniting MathJax, Jekyll and a Strict CSP
In our quest to vanquish the foe that is XSS and for an A+ rating from the Mozilla Observatory, today, we will be rendering mathematical formulas in a fast and secure fashion.
TL;DR: If you want to include math in your Jekyll blog that does neither tax your reader’s CPUs nor force you to forego a strict CSP, try out my new plugin jekyll-mathjax-csp
.
Motivation
This blog doubles as my personal testbed for web technologies. My aim is to try out new techniques without compromising on aspects of security. After all, how could I wholeheartedly recommend Content Security Policies, HSTS, usage of SRI and all the other web security features without having them set up on my own site? And even the excuse that this page is statically generated and thus will never be exposed to much harm doesn’t hold up, as comments, hosted JavaScript experiments and potentially malicious dependencies make for quite some dymanic content on this otherwise static web page.
MathJax — “Beautiful Math in All Browsers”
MathJax is a JavaScript library that renders mathematical formulas expressed in TeX (or MathML or ASCIIMath) directly in the user’s browser. It supports a wide variety of TeX commands, features typography that is (almost as) beautiful as that produced by native (La)TeX and puts effort into making the rendered output accessible.
Client-Side Rendering — “It’s complicated”
While MathJax is arguably the gold standard for rendering math on the web, it also has some disadvantages: Since the rendering happens client-side, it causes high CPU load on the user’s device, which can be overwhelming especially on cheaper mobile clients. At best, it will take a couple of seconds to render a math-heavy page.
Another potential problem of client-side MathJax rendering is that two of the three supported output formats, SVG and HTML, requires lots of inline style
attributes. As a result, a CSP on a page relying on MathJax necessarily has to include style-src: 'unsafe-inline'
. While being much less dangerous than script-src: 'unsafe-inline'
, this directive is still able to inflict damage, including but not limited to blatantly defacing, covertly changing and leaking information from a page.
The remaining output format, MathML, was created as a “native” markup language for math on the web, but has never really caught on: At the moment, only Firefox has full support for it, meaning that it cannot be used without providing a fallback to other formats. It certainly didn’t help MathML that it is quite complex, frequently making an appearance in arcane XSS vectors. Did you know that MathML can be used to build links that will always leak window.opener
? Or that it can be used to execute JavaScript without using an href
or event listener attribute? Since such attack vectors could also be exploited by MathML rendered from a TeX formula, MathJax offers a Safe mode that is highly recommended if you use MathJax to render user-supplied TeX.
But even without rendering to MathML, client-side MathJax on a page with a CSP that permits the use of eval()
has gotcha potential. When MathJax loads on a page for the first time, it looks for specially marked <script>
tags containing custom configurations. While these scripts usually consist of a single call to MathJax.Hub.Config
, they can contain arbitrary JavaScript that is executed by MathJax using eval()
. The result is an unsafe-eval
to unsafe-inline
CSP bypass with the restriction that inline scripts have to be injected into the page before MathJax is loaded. You can experience this bypass in action in this lab.
Server-Side Rendering — “SVG to the Rescue”
Given the restrictions that client-side MathJax imposes on the strictness of the CSP and the CPU load it causes on the reader’s device, I decided to try out server-side rendering. The npm package mathjax-node
can be used from Node.js to pre-render TeX formulas into either SVG, HTML or MathML. Together with the package mathjax-node-page
, entire pages of TeX math can be rendered automatically, either from Node.js or via the CLI command mjpage
.
While in HTML and MathML mode the server-based approach suffers from the same CSP-related problems as discussed above, the SVG output made me hopeful: Its usage of inline styles is limited to a single fixed <style>
element in the head and one style
attribute per SVG in the output. As this appears to be a somewhat manageable amount of inline activity and since SVG is a widely supported and high-quality output format for mathematical typography, I decided to wrap everything into a CSP-aware Jekyll plugin.
jekyll-mathjax-csp
jekyll-mathjax-csp
is a Jekyll plugin that automatically renders math picked up by the kramdown parser by running it through mathjax-node
. It collects all newly added inline styles and turns them into proper CSS classes, which are then injected into a single inline <style>
element in the head of the page. The hash over this element’s content is computed and the collection of all such hashes can either automatically (via the {% mathjax_csp_sources %}
tag) or manually (copied from a message shown during the build) be added to the site’s CSP.
Usage
Head over to the README for detailed usage instructions.
Issues
If you experience any issues with jekyll-mathjax-csp
or want to make a feature request, please create an issue on GitHub.
Examples
Inline math: $$\pi_3(S^2) \cong \mathbb{Z}$$
Display math:
$$\operatorname{ind} P = \int_{X}\operatorname{ch} \sigma(P) \cdot \operatorname{Td}(TX \otimes \mathbb{C})$$
CSP sources for all the MathJax-related styles on hen.ne.ke:
{% mathjax_csp_sources %}
'sha256-ERPndH5xUrcjZhkNII2aUCkaIx14Oi89jPe8IJN4TiY='