-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Set the nonce
attribute on inline style tags created by the output processor
#2665
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
Comments
I thought it was OK, but it turns out I just had an old version of Chrome and the I think that it'll need to go in an extension, so it can work as a postFilter, like the |
I remember looking into this before, and was pretty sure that Otherwise, yes, it appears you will have to make stylesheet entries for all the elements' The safe extension is one model for how to do that as an extension (using a postFilter on the output jax rather than the array of input jax as in that example). Another option would be to use a Finally, another potential approach would be to handle it via the DOMAdaptor itself. You could subclass the HTMLAdaptor and override the You would need to register your adaptor with |
Your last option is the one I spent an afternoon trying, but I couldn't work out how to join up the stylesheet insertion. It sounds like |
I didn't get to look at this until today, and it turns out that Fortunately, it can all be done through the DOM adaptor. I have put together the following example configuration that implements a Because MathJax adds and removes elements (like the ones used for measurements), this code also takes some pains to detect when DOM elements have been dropped, and to remove their CSS rules and internal data. That makes it practical to use in dynamic pages where the math may be added or removed from the page without ending up using up lots of memory for rules and data that have been removed. This garbage-collection process can probably be improved, but it is at least an attempt to remove unneeded data. MathJax = {
CSP: {nonce: 'hello'},
startup: {
ready() {
const {HTMLAdaptor} = MathJax._.adaptors.HTMLAdaptor;
const {Styles} = MathJax._.util.Styles;
const {combineDefaults} = MathJax._.components.global;
//
// Allow configuration of nonce via MathJax = {CSP: {nonce: '...'}}
//
combineDefaults(MathJax.config, 'CSP', {nonce: ''});
/*****************************************************************/
/*
* Class to hold styles for a given node, and the rule that specifies it.
*/
class CssItem {
css = new Styles(); // The style for the node
node = null; // The DOM node, when added to the DOM
rule = null; // The stylesheet rule for this DOM node
prev = 0; // The index of the pevious node in the list
next = 0; // The index of the next node in the list
constructor(cssText = '') {
if (cssText) this.css.parse(cssText);
}
}
/*****************************************************************/
/*
* Class to hold a linke list of CssItem data.
*/
class CssList {
list = [new CssItem()]; // The list of items (the first is the root item)
root = this.list[0]; // The root item (next is start of list, prev is end of list)
free = []; // The list of free indices (for reuse)
/*
* The length of the list (not counting empty slots).
*/
get length() {
return this.list.length - this.free.length - 1;
}
/*
* The data for the nth item.
*/
get(n) {
if (n < 1) return null;
return this.list[n];
}
/*
* Add a new item with the given CSS string.
*/
add(css) {
const n = (this.free.length ? this.free.pop() : this.list.length);
const item = new CssItem(css);
this.list[n] = item;
item.next = 0;
item.prev = this.root.prev;
this.list[item.prev].next = n;
this.root.prev = n;
return n;
}
/*
* Remove the nth item from the list.
*/
remove(n) {
const item = this.list[n];
if (!item || n === 0) return;
this.list[item.prev].next = item.next;
this.list[item.next].prev = item.prev;
this.free.push(n);
this.list[n] = null;
}
}
/*****************************************************************/
/*
* A sublcass of HTMLAdaptor that removes in-line styles and uses a
* stylesheet for the styles instead (for better CSP support).
*/
class CSPAdaptor extends HTMLAdaptor {
nodes = new CssList(); // The list of CssItems for the nodes with CSS
domCount = 0; // The number of such nodes in the DOM
dataName = 'data-mjx-css-id'; // The attribute to use for the node id
dataSelector = `[${this.dataName}]`; // The selector to target the nodes with ids
sheet = null; // The stylesheet object for the CSP styles
nonde = ''; // The nonce to use for stylesheets
removeId = 0; // The next id to check for removal
removeCount = 10; // The number of nodes to check for removal at one time
/*
* Do the usual constructor and then create the style sheet.
*/
constructor(window, nonce = '') {
super(window);
const style = window.document.head.appendChild(window.document.createElement('style'));
style.setAttribute('id', 'CSP-STYLES');
if (nonce) style.setAttribute('nonce', nonce);
this.sheet = style.sheet;
this.nonce = nonce;
}
/*
* Get the CssItem for the given node, or create one with the given CSS (if given).
*/
getCSS(node, css = null) {
let item = this.nodes.get(node.getAttribute(this.dataName));
if (item) return item;
if (css === null) return new CssItem();
const n = this.nodes.add(css);
node.setAttribute(this.dataName, n);
return this.nodes.get(n);
}
/*
* Return an array of nodes that have styles in the tree rooted at the given node.
*/
getCssNodes(node) {
const nodes = Array.from(node.querySelectorAll(this.dataSelector));
if (node.getAttribute(this.dataName)) nodes.push(node);
return nodes;
}
/*
* Modify the CssItem's css, and if there is an active rule for this item, modify that as well.
*/
setStyle(node, name, value) {
const item = this.getCSS(node, '');
item.css.set(name, value);
if (item.rule) {
item.rule.style[name] = value;
}
}
/*
* Get the node's style value from the CssItem style.
*/
getStyle(node, name) {
const css = this.getCSS(node).css;
return css.get(name) || '';
}
/*
* Get the cssText from the CssItem for this node.
*/
allSyles(node) {
const css = this.getCSS(node).css;
return css.cssText;
}
/*
* If any cloned nodes have styles, give them separate ids and copy the styles.
*/
clone(node1) {
const node2 = super.clone(node1);
for (const node of this.getCssNodes(node2)) {
const id = node.getAttribute(this.dataName);
node.removeAttribute(this.dataName);
this.getCSS(node, this.nodes.get(id).css.cssText);
}
return node2;
}
/*
* Try to clean up DOM elements that may have been removed.
* We look through the list for items that were once in the DOM
* but aren't any longer (but only look through a few at a time,
* for efficiency). This could be made more sophisticated, e.g.
* keeping the list of nodes that are in the DOM separate from
* those that aren't, or not counting the ones it actually removes
* so that large strings of dead nodes could be removed at once.
* This is probably good enough for now.
*/
checkRemoved() {
let n = Math.min(this.domCount, this.removeCount);
let m = this.nodes.length;
while (n && m--) {
if (this.removeId === 0) {
this.removeId = this.nodes.root.next;
}
const item = this.nodes.get(this.removeId);
if (item.node) {
if (!this.window.document.contains(item.node)) {
this.nodes.remove(this.removeId);
item.node.removeAttribute(this.dataName);
if (item.rule) {
this.sheet.deleteRule(Array.from(this.sheet.cssRules).indexOf(item.rule));
}
this.domCount--;
}
n--;
}
this.removeId = item.next;
}
}
/*
* When a child is added to a parent, check if the parent is in the DOM.
* If so, do a removal check (this is our garbage collection call).
* Then for each node that has manages styles,
* If the node is not already in the DOM
* Insert the new style into the style sheet
* and record the style rule and node in the CssItem.
*/
updateCSS(parent, child) {
if (parent && this.window.document.contains(parent)) {
this.checkRemoved();
for (const node of this.getCssNodes(child)) {
const id = node.getAttribute(this.dataName);
const item = this.nodes.get(id);
if (!item.node) {
const rule = `[${this.dataName}="${id}"] {${item.css.cssText}}`;
this.sheet.insertRule(rule, 0);
item.rule = this.sheet.cssRules[0];
item.node = node;
this.domCount++;
}
}
}
}
/*
* Add nonce to any stylesheets (handles both CHTML and SVG output).
*/
addNonce(node) {
if (this.nonce && node.nodeName === 'STYLE') {
node.setAttribute('nonce', this.nonce);
}
}
/*
* If the child is being added to the active DOM, update the CSS first.
*/
append(parent, child) {
this.updateCSS(parent, child);
this.addNonce(child);
return super.append(parent, child);
}
/*
* If the child is being added to the active DOM, update the CSS first.
*/
insert(parent, child) {
this.updateCSS(parent, child);
this.addNonce(child);
return super.insert(parent, child);
}
/*
* If the new node is being added to the active DOM, update the CSS first.
*/
replace(nnode, onode) {
this.updateCSS(onode.parentNode, nnode);
this.addNonce(nnode);
return super.replace(nnode, onode);
}
}
/*****************************************************************/
//
// Have the browerAdaptor use the new CSPAdaptor.
//
MathJax.startup.registerConstructor("browserAdaptor", () => new CSPAdaptor(window, MathJax.config.CSP.nonce));
//
// Do the usual startup
//
MathJax.startup.defaultReady();
}
}
}; Of course, this should be made into an actual extension, but this setup allows you to modify and test the code easily without having to webpack extensions for every change. In any case, you may find it a useful example for your efforts. |
Davide I found this when trying to settle the security issues with that demo page I worked on for Bernd. I used your code snippet which worked fine and added nonce's to the styles that MathJax adds. However, the nonce additions came too late (for the setup I was using on that page) and Chrome had already tried and rejected applying the styles. (I ended up having to use 'unsafe-inline' in the header, which kind of negates the point of security.) Is it possible to add an option where the developer provides an nonce value in the setup like you have above, but with an extra flag that just says basically to add the nonce tags as the styles are added to the page? |
@mbourne, I'm not sure I understand the request. The current code adds the none to the style tags before they are inserted into the page, and there are no explicit Can you be more specific about what you are doing? |
Sorry, but I realise now that the above code is not meant to do what I'm talking about. Your questions address exactly what I mean. My question comes from the fact that on that page for Bernd, the styles that were added do not include any nonce's. The first style tag is my own (with the nonce that I applied), and the other 3 were added by MathJax, and do not have nonce's. In an attempt to rectify this, I tried to add the nonce to each style tag after it was created, and that didn't do anything as far as applying the styles to the page. Then I tried removing the styles and creating new ones, this time with nonce's. This time the browser recognised them (I assume by the appearance in DOM inspector), but the styles were not applied to the page. This is the page where I'm doing this (I removed 'unsafe-inline' in the headers for this page): https://bourne2learn.com/cg3/peu/neural_networks_backpropagation-styles-nonce3.php So back to my initial question. How do I tell the startup sequence what my nonce value is, and have it applied to the styles created by MathJax? |
@mbourne, there was additional information in the PR that Christian provided, and it indicates that there are styles generated by the MathJax menu that you will not be able to manage in this way. The three stylesheets that you mention are generated by the menu framework, and can't be fixed in an easy way. Even if you could nonce those stylesheets, the menu code uses inline styles that don't pass through the DOMadaptor that we have subclassed above, and so this will not catch those, and you will still get errors. You will not be able to include the menu when using this approach, and since the menu its included in the tex-chtml component that you are using, you will need to change that to load the needed pieces explicitly and using the startup component instead. Something like: <script>
MathJax = {
loader: {load: ['input/tex', 'output/chtml']},
... (the rest of the stuff from above) ...
};
</script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/startup"></script> Here I've left out the menu component and the assistive MathML component, which may also cause problems under certain circumstances (the MathML could include styles, which will not be trapped through the code above). |
@mbourne: I've looked closer at your page and noticed a few things that may be contributing to your issues. First, you haven't included the nonce in the |
Content security policies can specify a value for the
nonce
that must be present on all inline styles, in thestyle-src
part of the directive. When a nonce is specified, theunsafe-inline
directive is ignored.The idea is to prevent styles included in user-supplied content from being applied. The nonce should be different each time the page is loaded, so this only protects against content that doesn't change with each page load.
The Google closure library resolves this by finding a
<script>
tag in the page with thenonce
attribute, and using that. There isn't always one of those, so it should be possible to pass it as an option in the MathJax config, too.I'm looking at this today to get the MathJax integration on mastodon working, so I might have a pull request soon.
The text was updated successfully, but these errors were encountered: