Merge branch 'security-katex-dos-master' into 'master'
Enforce max chars and max render time in markdown math See merge request gitlab/gitlabhq!3277
This commit is contained in:
commit
4ed9802a40
|
@ -1,6 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import { __ } from '~/locale';
|
||||
import flash from '~/flash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
||||
// Renders math using KaTeX in any element with the
|
||||
// `js-render-math` class
|
||||
|
@ -10,21 +9,131 @@ import flash from '~/flash';
|
|||
// <code class="js-render-math"></div>
|
||||
//
|
||||
|
||||
// Loop over all math elements and render math
|
||||
function renderWithKaTeX(elements, katex) {
|
||||
elements.each(function katexElementsLoop() {
|
||||
const mathNode = $('<span></span>');
|
||||
const $this = $(this);
|
||||
const MAX_MATH_CHARS = 1000;
|
||||
const MAX_RENDER_TIME_MS = 2000;
|
||||
|
||||
const display = $this.attr('data-math-style') === 'display';
|
||||
try {
|
||||
katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false });
|
||||
mathNode.insertAfter($this);
|
||||
$this.remove();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
// These messages might be used with inline errors in the future. Keep them around. For now, we will
|
||||
// display a single error message using flash().
|
||||
|
||||
// const CHAR_LIMIT_EXCEEDED_MSG = sprintf(
|
||||
// s__(
|
||||
// 'math|The following math is too long. For performance reasons, math blocks are limited to %{maxChars} characters. Try splitting up this block, or include an image instead.',
|
||||
// ),
|
||||
// { maxChars: MAX_MATH_CHARS },
|
||||
// );
|
||||
// const RENDER_TIME_EXCEEDED_MSG = s__(
|
||||
// "math|The math in this entry is taking too long to render. Any math below this point won't be shown. Consider splitting it among multiple entries.",
|
||||
// );
|
||||
|
||||
const RENDER_FLASH_MSG = sprintf(
|
||||
s__(
|
||||
'math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead.',
|
||||
),
|
||||
{ maxChars: MAX_MATH_CHARS },
|
||||
);
|
||||
|
||||
// Wait for the browser to reflow the layout. Reflowing SVG takes time.
|
||||
// This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object".
|
||||
const waitForReflow = fn => {
|
||||
window.requestAnimationFrame(fn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders math blocks sequentially while protecting against DoS attacks. Math blocks have a maximum character limit of MAX_MATH_CHARS. If rendering math takes longer than MAX_RENDER_TIME_MS, all subsequent math blocks are skipped and an error message is shown.
|
||||
*/
|
||||
class SafeMathRenderer {
|
||||
/*
|
||||
How this works:
|
||||
|
||||
The performance bottleneck in rendering math is in the browser trying to reflow the generated SVG.
|
||||
During this time, the JS is blocked and the page becomes unresponsive.
|
||||
We want to render math blocks one by one until a certain time is exceeded, after which we stop
|
||||
rendering subsequent math blocks, to protect against DoS. However, browsers do reflowing in an
|
||||
asynchronous task, so we can't time it synchronously.
|
||||
|
||||
SafeMathRenderer essentially does the following:
|
||||
1. Replaces all math blocks with placeholders so that they're not mistakenly rendered twice.
|
||||
2. Places each placeholder element in a queue.
|
||||
3. Renders the element at the head of the queue and waits for reflow.
|
||||
4. After reflow, gets the elapsed time since step 3 and repeats step 3 until the queue is empty.
|
||||
*/
|
||||
queue = [];
|
||||
totalMS = 0;
|
||||
|
||||
constructor(elements, katex) {
|
||||
this.elements = elements;
|
||||
this.katex = katex;
|
||||
|
||||
this.renderElement = this.renderElement.bind(this);
|
||||
this.render = this.render.bind(this);
|
||||
}
|
||||
|
||||
renderElement() {
|
||||
if (!this.queue.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = this.queue.shift();
|
||||
const text = el.textContent;
|
||||
|
||||
el.removeAttribute('style');
|
||||
|
||||
if (this.totalMS >= MAX_RENDER_TIME_MS || text.length > MAX_MATH_CHARS) {
|
||||
if (!this.flashShown) {
|
||||
flash(RENDER_FLASH_MSG);
|
||||
this.flashShown = true;
|
||||
}
|
||||
|
||||
// Show unrendered math code
|
||||
const codeElement = document.createElement('pre');
|
||||
codeElement.className = 'code';
|
||||
codeElement.textContent = el.textContent;
|
||||
el.parentNode.replaceChild(codeElement, el);
|
||||
|
||||
// Render the next math
|
||||
this.renderElement();
|
||||
} else {
|
||||
this.startTime = Date.now();
|
||||
|
||||
try {
|
||||
el.innerHTML = this.katex.renderToString(text, {
|
||||
displayMode: el.getAttribute('data-math-style') === 'display',
|
||||
throwOnError: true,
|
||||
maxSize: 20,
|
||||
maxExpand: 20,
|
||||
});
|
||||
} catch {
|
||||
// Don't show a flash for now because it would override an existing flash message
|
||||
el.textContent = s__('math|There was an error rendering this math block');
|
||||
// el.style.color = '#d00';
|
||||
el.className = 'katex-error';
|
||||
}
|
||||
|
||||
// Give the browser time to reflow the svg
|
||||
waitForReflow(() => {
|
||||
const deltaTime = Date.now() - this.startTime;
|
||||
this.totalMS += deltaTime;
|
||||
|
||||
this.renderElement();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// Replace math blocks with a placeholder so they aren't rendered twice
|
||||
this.elements.forEach(el => {
|
||||
const placeholder = document.createElement('span');
|
||||
placeholder.style.display = 'none';
|
||||
placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style'));
|
||||
placeholder.textContent = el.textContent;
|
||||
el.parentNode.replaceChild(placeholder, el);
|
||||
this.queue.push(placeholder);
|
||||
});
|
||||
|
||||
// If we wait for the browser thread to settle down a bit, math rendering becomes 5-10x faster
|
||||
// and less prone to timeouts.
|
||||
setTimeout(this.renderElement, 400);
|
||||
}
|
||||
}
|
||||
|
||||
export default function renderMath($els) {
|
||||
|
@ -34,7 +143,8 @@ export default function renderMath($els) {
|
|||
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
|
||||
])
|
||||
.then(([katex]) => {
|
||||
renderWithKaTeX($els, katex);
|
||||
const renderer = new SafeMathRenderer($els.get(), katex);
|
||||
renderer.render();
|
||||
})
|
||||
.catch(() => flash(__('An error occurred while rendering KaTeX')));
|
||||
.catch(() => {});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enforce max chars and max render time in markdown math
|
||||
merge_request:
|
||||
author:
|
||||
type: security
|
|
@ -1101,9 +1101,6 @@ msgstr ""
|
|||
msgid "An error occurred while parsing recent searches"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while rendering KaTeX"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while rendering preview broadcast message"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13676,6 +13673,12 @@ msgstr ""
|
|||
msgid "manual"
|
||||
msgstr ""
|
||||
|
||||
msgid "math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead."
|
||||
msgstr ""
|
||||
|
||||
msgid "math|There was an error rendering this math block"
|
||||
msgstr ""
|
||||
|
||||
msgid "merge request"
|
||||
msgid_plural "merge requests"
|
||||
msgstr[0] ""
|
||||
|
|
|
@ -34,7 +34,9 @@ describe 'Math rendering', :js do
|
|||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
expect(page).to have_selector('.katex-error', text: "\href{javascript:alert('xss');}{xss}")
|
||||
page.within '.description > .md' do
|
||||
expect(page).to have_selector('.katex-error')
|
||||
expect(page).to have_selector('.katex-html a', text: 'Gitlab')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue