From ed1a50099f02ad62cd226036b7d8e6b63ee79c66 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Mon, 2 Oct 2023 11:58:35 -0500 Subject: [PATCH 1/4] Add option to turn off math rendering limits This allows self-managed instances to better use complicated math when in a trusted environment. Changelog: added --- .../behaviors/markdown/render_math.js | 46 ++++--- app/helpers/application_settings_helper.rb | 1 + app/models/application_setting.rb | 3 + .../application_setting_implementation.rb | 1 + ...51451_add_math_rendering_limits_enabled.rb | 9 ++ db/schema_migrations/20230929151451 | 1 + db/structure.sql | 1 + doc/administration/instance_limits.md | 18 +++ lib/banzai/filter/math_filter.rb | 14 +- lib/gitlab/gon_helper.rb | 19 +-- spec/features/markdown/math_spec.rb | 127 +++++++++++------- spec/lib/banzai/filter/math_filter_spec.rb | 17 ++- spec/models/application_setting_spec.rb | 3 + 13 files changed, 179 insertions(+), 81 deletions(-) create mode 100644 db/migrate/20230929151451_add_math_rendering_limits_enabled.rb create mode 100644 db/schema_migrations/20230929151451 diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index 7525fc76d169b4..d2df58feaf4f22 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -60,7 +60,10 @@ class SafeMathRenderer { } const el = chosenEl || this.queue.shift(); - const forceRender = Boolean(chosenEl) || unrestrictedPages.includes(this.pageName); + const forceRender = + Boolean(chosenEl) || + unrestrictedPages.includes(this.pageName) || + !gon.math_rendering_limits_enabled; const text = el.textContent; el.removeAttribute('style'); @@ -128,20 +131,7 @@ class SafeMathRenderer { } // eslint-disable-next-line no-unsanitized/property - displayContainer.innerHTML = this.katex.renderToString(text, { - displayMode: el.dataset.mathStyle === 'display', - throwOnError: true, - maxSize: MAX_USER_SPECIFIED_EMS, - // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111107 for - // reasoning behind this value - maxExpand: MAX_MACRO_EXPANSIONS, - trust: (context) => - // this config option restores the KaTeX pre-v0.11.0 - // behavior of allowing certain commands and protocols - // eslint-disable-next-line @gitlab/require-i18n-strings - ['\\url', '\\href'].includes(context.command) && - ['http', 'https', 'mailto', '_relative'].includes(context.protocol), - }); + displayContainer.innerHTML = this.katex.renderToString(text, this.katexOptions(el)); } catch (e) { // Don't show a flash for now because it would override an existing flash message if (e.message.match(/Too many expansions/)) { @@ -205,6 +195,32 @@ class SafeMathRenderer { } }); } + + // eslint-disable-next-line class-methods-use-this + katexOptions(el) { + const options = { + displayMode: el.dataset.mathStyle === 'display', + throwOnError: true, + trust: (context) => + // this config option restores the KaTeX pre-v0.11.0 + // behavior of allowing certain commands and protocols + // eslint-disable-next-line @gitlab/require-i18n-strings + ['\\url', '\\href'].includes(context.command) && + ['http', 'https', 'mailto', '_relative'].includes(context.protocol), + }; + + if (gon.math_rendering_limits_enabled) { + options.maxSize = MAX_USER_SPECIFIED_EMS; + // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111107 for + // reasoning behind this value + options.maxExpand = MAX_MACRO_EXPANSIONS; + } else { + // eslint-disable-next-line @gitlab/require-i18n-strings + options.maxExpand = 'Infinity'; + } + + return options; + } } export default function renderMath(elements) { diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 0fd232a6088898..89e77bde32529b 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -314,6 +314,7 @@ def visible_attributes :jira_connect_application_key, :jira_connect_public_key_storage_enabled, :jira_connect_proxy_url, + :math_rendering_limits_enabled, :max_artifacts_size, :max_attachment_size, :max_export_size, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 4e3c2028c546ca..4da3f0479360bd 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -830,6 +830,9 @@ def self.kroki_formats_attributes allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } + validates :math_rendering_limits_enabled, + inclusion: { in: [true, false], message: N_('must be a boolean value') } + before_validation :ensure_uuid! before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed? before_validation :normalize_default_branch_name diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 18f1a53f8aaea7..e6d65a8b70598a 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -118,6 +118,7 @@ def defaults # rubocop:disable Metrics/AbcSize login_recaptcha_protection_enabled: false, mailgun_signing_key: nil, mailgun_events_enabled: false, + math_rendering_limits_enabled: true, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], max_export_size: 0, diff --git a/db/migrate/20230929151451_add_math_rendering_limits_enabled.rb b/db/migrate/20230929151451_add_math_rendering_limits_enabled.rb new file mode 100644 index 00000000000000..d52019c41ed7dd --- /dev/null +++ b/db/migrate/20230929151451_add_math_rendering_limits_enabled.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddMathRenderingLimitsEnabled < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def change + add_column :application_settings, :math_rendering_limits_enabled, :boolean, default: true, null: false + end +end diff --git a/db/schema_migrations/20230929151451 b/db/schema_migrations/20230929151451 new file mode 100644 index 00000000000000..fbc9c908c91603 --- /dev/null +++ b/db/schema_migrations/20230929151451 @@ -0,0 +1 @@ +414442e7daf9e23bb33c8471f30c6465bcef6a19051d609888f5bbf88bb5306e \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 90dafad1dc7f78..78864f3810eca3 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11854,6 +11854,7 @@ CREATE TABLE application_settings ( encrypted_vertex_ai_access_token bytea, encrypted_vertex_ai_access_token_iv bytea, project_jobs_api_rate_limit integer DEFAULT 600 NOT NULL, + math_rendering_limits_enabled boolean DEFAULT true NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index 377a3dba6c9604..3e5acf52a388c1 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -990,6 +990,24 @@ You can configure this limit for self-managed installations when you [enable Elasticsearch](../integration/advanced_search/elasticsearch.md#enable-advanced-search). Set the limit to `0` to disable it. +## Math rendering limits + +By default, GitLab imposes limits when rendering math in Markdown fields for better security and performance. The current limits are: + +- issues/MRs/wikis/repos: maximum number of nodes rendered is `50` +- issues/MRs/wikis/repos: maximum number of macro expansions is `1000` +- issues/MRs/wikis/repos: maximum user-specified sizes is `20ems` +- issues/MRs: maximum number of characters in a math block is `1000` +- issues/MRs: maximum amount of time it takes to render is `2000ms` + +You can disable these limits when running in a self-managed instance and you trust the user input. + +Use the [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): + +```ruby +ApplicationSetting.update(math_rendering_limits_enabled: false) +``` + ## Wiki limits - [Wiki page content size limit](wikis/index.md#wiki-page-content-size-limit). diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb index e568f51652fef3..3161e030194e83 100644 --- a/lib/banzai/filter/math_filter.rb +++ b/lib/banzai/filter/math_filter.rb @@ -47,7 +47,7 @@ def call # Add necessary classes to any existing math blocks def process_existing doc.xpath(XPATH_INLINE_CODE).each do |code| - break if @nodes_count >= RENDER_NODES_LIMIT + break if render_nodes_limit_reached?(@nodes_count) code[:class] = MATH_CLASSES @@ -58,7 +58,7 @@ def process_existing # Corresponds to the "$`...`$" syntax def process_dollar_backtick_inline doc.xpath(XPATH_CODE).each do |code| - break if @nodes_count >= RENDER_NODES_LIMIT + break if render_nodes_limit_reached?(@nodes_count) closing = code.next opening = code.previous @@ -87,6 +87,16 @@ def process_math_codeblock pre_node[:class] = TAG_CLASS end end + + def settings + Gitlab::CurrentSettings.current_application_settings + end + + def render_nodes_limit_reached?(count) + return false unless settings.math_rendering_limits_enabled? + + count >= RENDER_NODES_LIMIT + end end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index a7f8b93ee102da..168f3aa4c597cd 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -7,15 +7,16 @@ module GonHelper include WebpackHelper def add_gon_variables - gon.api_version = 'v4' - gon.default_avatar_url = default_avatar_url - gon.max_file_size = Gitlab::CurrentSettings.max_attachment_size - gon.asset_host = ActionController::Base.asset_host - gon.webpack_public_path = webpack_public_path - gon.relative_url_root = Gitlab.config.gitlab.relative_url_root - gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class - gon.markdown_surround_selection = current_user&.markdown_surround_selection - gon.markdown_automatic_lists = current_user&.markdown_automatic_lists + gon.api_version = 'v4' + gon.default_avatar_url = default_avatar_url + gon.max_file_size = Gitlab::CurrentSettings.max_attachment_size + gon.asset_host = ActionController::Base.asset_host + gon.webpack_public_path = webpack_public_path + gon.relative_url_root = Gitlab.config.gitlab.relative_url_root + gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class + gon.markdown_surround_selection = current_user&.markdown_surround_selection + gon.markdown_automatic_lists = current_user&.markdown_automatic_lists + gon.math_rendering_limits_enabled = Gitlab::CurrentSettings.math_rendering_limits_enabled add_browsersdk_tracking diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb index 0bc8f2146e9b6c..0d12aade807866 100644 --- a/spec/features/markdown/math_spec.rb +++ b/spec/features/markdown/math_spec.rb @@ -48,47 +48,92 @@ end end - it 'renders lazy load button' do - description = <<~MATH - ```math - \Huge \sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} - ``` - MATH - - create_and_visit_issue_with_description(description) + describe 'applying limits on math rendering' do + let(:lazy_load_description) do + <<~MATH + ```math + \Huge \sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} + ``` + MATH + end - page.within '.description > .md' do - expect(page).to have_selector('.js-lazy-render-math-container', text: /math block exceeds 1000 characters/) + let(:excessive_expansion_description) do + <<~MATH + ```math + #{'\\mod e ' * 150} + ``` + MATH end - end - it 'allows many expansions', :js do - description = <<~MATH - ```math - #{'\\mod e ' * 100} - ``` - MATH + context 'when limits should be applied (default)' do + it 'renders lazy load button' do + create_and_visit_issue_with_description(lazy_load_description) - create_and_visit_issue_with_description(description) + page.within '.description > .md' do + expect(page).to have_selector('.js-lazy-render-math-container', text: /math block exceeds 1000 characters/) + end + end - page.within '.description > .md' do - expect(page).not_to have_selector('.katex-error') + it 'allows many expansions', :js do + description = <<~MATH + ```math + #{'\\mod e ' * 100} + ``` + MATH + + create_and_visit_issue_with_description(description) + + page.within '.description > .md' do + expect(page).not_to have_selector('.katex-error') + end + end + + it 'shows error message when too many expansions', :js do + create_and_visit_issue_with_description(excessive_expansion_description) + + page.within '.description > .md' do + click_button 'Display anyway' + + expect(page).to have_selector('.katex-error', text: /Too many expansions/) + end + end + + it 'renders without any limits on wiki page', :js do + wiki_page = build(:wiki_page, { container: project, content: lazy_load_description }) + wiki_page.create message: 'math test commit' # rubocop:disable Rails/SaveBang + wiki_page = project.wiki.find_page(wiki_page.slug) + + visit project_wiki_path(project, wiki_page) + + wait_for_requests + + page.within '.js-wiki-page-content' do + expect(page).not_to have_selector('.js-lazy-render-math') + end + end end - end - it 'shows error message when too many expansions', :js do - description = <<~MATH - ```math - #{'\\mod e ' * 150} - ``` - MATH + context 'when limits are disabled' do + before do + stub_application_setting(math_rendering_limits_enabled: false) + end - create_and_visit_issue_with_description(description) + it 'does not render lazy load button' do + create_and_visit_issue_with_description(lazy_load_description) - page.within '.description > .md' do - click_button 'Display anyway' + page.within '.description > .md' do + expect(page) + .not_to have_selector('.js-lazy-render-math-container', text: /math block exceeds 1000 characters/) + end + end - expect(page).to have_selector('.katex-error', text: /Too many expansions/) + it 'does not show error message when too many expansions', :js do + create_and_visit_issue_with_description(excessive_expansion_description) + + page.within '.description > .md' do + expect(page).not_to have_selector('.katex-error', text: /Too many expansions/) + end + end end end @@ -121,26 +166,6 @@ end end - it 'renders without any limits on wiki page', :js do - description = <<~MATH - ```math - \Huge \sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} - ``` - MATH - - wiki_page = build(:wiki_page, { container: project, content: description }) - wiki_page.create message: 'math test commit' # rubocop:disable Rails/SaveBang - wiki_page = project.wiki.find_page(wiki_page.slug) - - visit project_wiki_path(project, wiki_page) - - wait_for_requests - - page.within '.js-wiki-page-content' do - expect(page).not_to have_selector('.js-lazy-render-math') - end - end - it 'uses math-content-display for display math', :js do description = <<~MATH ```math diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb index e4ebebc0fdebf5..3fa0f9028e8c15 100644 --- a/spec/lib/banzai/filter/math_filter_spec.rb +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -207,12 +207,21 @@ expect(doc.search('[data-math-style="display"]').count).to eq(1) end - it 'limits how many elements can be marked as math' do - stub_const('Banzai::Filter::MathFilter::RENDER_NODES_LIMIT', 2) + context 'when limiting how many elements can be marked as math' do + subject { pipeline_filter('$`2+2`$ + $3+3$ + $$4+4$$') } - doc = pipeline_filter('$`2+2`$ + $3+3$ + $$4+4$$') + it 'enforces limits by default' do + stub_const('Banzai::Filter::MathFilter::RENDER_NODES_LIMIT', 2) + + expect(subject.search('.js-render-math').count).to eq(2) + end - expect(doc.search('.js-render-math').count).to eq(2) + it 'does not limit when math_rendering_limits_enabled is false' do + stub_application_setting(math_rendering_limits_enabled: false) + stub_const('Banzai::Filter::MathFilter::RENDER_NODES_LIMIT', 2) + + expect(subject.search('.js-render-math').count).to eq(3) + end end it 'protects against malicious backtracking' do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index ac07755e29c183..1d9e0db259e6f9 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -297,6 +297,9 @@ def many_usernames(num = 100) it { is_expected.to validate_inclusion_of(:package_registry_allow_anyone_to_pull_option).in_array([true, false]) } + it { is_expected.to allow_value([true, false]).for(:math_rendering_limits_enabled) } + it { is_expected.not_to allow_value(nil).for(:math_rendering_limits_enabled) } + context 'when deactivate_dormant_users is enabled' do before do stub_application_setting(deactivate_dormant_users: true) -- GitLab From 516d0c35a1faeb9cdaf5e5fc194486ab2fa94ed6 Mon Sep 17 00:00:00 2001 From: Amy Qualls Date: Thu, 5 Oct 2023 15:31:31 +0000 Subject: [PATCH 2/4] Apply documentation reviewer suggestions --- doc/administration/instance_limits.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index 3e5acf52a388c1..b412512a1d3d50 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -992,13 +992,20 @@ Set the limit to `0` to disable it. ## Math rendering limits -By default, GitLab imposes limits when rendering math in Markdown fields for better security and performance. The current limits are: +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132939) in GitLab 16.5. -- issues/MRs/wikis/repos: maximum number of nodes rendered is `50` -- issues/MRs/wikis/repos: maximum number of macro expansions is `1000` -- issues/MRs/wikis/repos: maximum user-specified sizes is `20ems` -- issues/MRs: maximum number of characters in a math block is `1000` -- issues/MRs: maximum amount of time it takes to render is `2000ms` +GitLab imposes default limits when rendering math in Markdown fields. These limits provide better security and performance. + +The limits for issues, merge requests, wikis, and repositories: + +- Maximum number of nodes rendered: `50`. +- Maximum number of macro expansions: `1000`. +- Maximum user-specified size: `20 em`. + +The limits for issues and merge requests: + +- Maximum number of characters in a math block: `1000`. +- Maximum rendering time: `2000 ms`. You can disable these limits when running in a self-managed instance and you trust the user input. -- GitLab From 9a16f30e7e9427491bc3e0a68cbd6bb07e6892c4 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Fri, 6 Oct 2023 10:17:47 -0500 Subject: [PATCH 3/4] Apply reviewer suggestions --- .../behaviors/markdown/render_math.js | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index d2df58feaf4f22..4cba3eccb45a64 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -22,6 +22,31 @@ const waitForReflow = (fn) => { window.requestIdleCallback(fn); }; +const katexOptions = (el) => { + const options = { + displayMode: el.dataset.mathStyle === 'display', + throwOnError: true, + trust: (context) => + // this config option restores the KaTeX pre-v0.11.0 + // behavior of allowing certain commands and protocols + // eslint-disable-next-line @gitlab/require-i18n-strings + ['\\url', '\\href'].includes(context.command) && + ['http', 'https', 'mailto', '_relative'].includes(context.protocol), + }; + + if (gon.math_rendering_limits_enabled) { + options.maxSize = MAX_USER_SPECIFIED_EMS; + // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111107 for + // reasoning behind this value + options.maxExpand = MAX_MACRO_EXPANSIONS; + } else { + // eslint-disable-next-line @gitlab/require-i18n-strings + options.maxExpand = 'Infinity'; + } + + return options; +}; + /** * 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. */ @@ -131,7 +156,7 @@ class SafeMathRenderer { } // eslint-disable-next-line no-unsanitized/property - displayContainer.innerHTML = this.katex.renderToString(text, this.katexOptions(el)); + displayContainer.innerHTML = this.katex.renderToString(text, katexOptions(el)); } catch (e) { // Don't show a flash for now because it would override an existing flash message if (e.message.match(/Too many expansions/)) { @@ -195,32 +220,6 @@ class SafeMathRenderer { } }); } - - // eslint-disable-next-line class-methods-use-this - katexOptions(el) { - const options = { - displayMode: el.dataset.mathStyle === 'display', - throwOnError: true, - trust: (context) => - // this config option restores the KaTeX pre-v0.11.0 - // behavior of allowing certain commands and protocols - // eslint-disable-next-line @gitlab/require-i18n-strings - ['\\url', '\\href'].includes(context.command) && - ['http', 'https', 'mailto', '_relative'].includes(context.protocol), - }; - - if (gon.math_rendering_limits_enabled) { - options.maxSize = MAX_USER_SPECIFIED_EMS; - // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111107 for - // reasoning behind this value - options.maxExpand = MAX_MACRO_EXPANSIONS; - } else { - // eslint-disable-next-line @gitlab/require-i18n-strings - options.maxExpand = 'Infinity'; - } - - return options; - } } export default function renderMath(elements) { -- GitLab From 8f772bf15cce39b831bb88eb1bb3910dd9d485c9 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Thu, 12 Oct 2023 07:08:13 -0500 Subject: [PATCH 4/4] Apply reviewer suggestions --- doc/administration/instance_limits.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index b412512a1d3d50..8f03a2224ecc01 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -1000,12 +1000,12 @@ The limits for issues, merge requests, wikis, and repositories: - Maximum number of nodes rendered: `50`. - Maximum number of macro expansions: `1000`. -- Maximum user-specified size: `20 em`. +- Maximum user-specified size in em: `20`. The limits for issues and merge requests: - Maximum number of characters in a math block: `1000`. -- Maximum rendering time: `2000 ms`. +- Maximum rendering time: `2000 ms`. You can disable these limits when running in a self-managed instance and you trust the user input. -- GitLab