diff --git a/.eslintrc.yml b/.eslintrc.yml index af2f1d88938..659ed2a0010 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,6 +3,7 @@ extends: - plugin:@gitlab/i18n - plugin:no-jquery/slim - plugin:no-jquery/deprecated-3.4 + - plugin:no-unsanitized/DOM - ./tooling/eslint-config/conditionally_ignore.js globals: __webpack_public_path__: true @@ -116,6 +117,14 @@ rules: vue/multi-word-component-names: off unicorn/prefer-dom-node-dataset: - error + no-unsanitized/method: + - error + - escape: + methods: 'sanitize' + no-unsanitized/property: + - error + - escape: + methods: 'sanitize' overrides: - files: - '{,ee/,jh/}spec/frontend*/**/*' @@ -134,6 +143,8 @@ overrides: message: 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.' - selector: ImportSpecifier[imported.name='GlSkeletonLoading'] message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.' + no-unsanitized/method: off + no-unsanitized/property: off - files: - 'config/**/*' - 'scripts/**/*' diff --git a/Gemfile b/Gemfile index 245a57935dc..fb8e53be369 100644 --- a/Gemfile +++ b/Gemfile @@ -482,7 +482,7 @@ gem 'net-ntp' gem 'ssh_data', '~> 1.3' # Spamcheck GRPC protocol definitions -gem 'spamcheck', '~> 0.1.0' +gem 'spamcheck', '~> 1.0.0' # Gitaly GRPC protocol definitions gem 'gitaly', '~> 15.3.0-rc4' diff --git a/Gemfile.lock b/Gemfile.lock index 6f676dbef2e..d9e9b2f04a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1314,7 +1314,7 @@ GEM sorted_set (1.0.3) rbtree set (~> 1.0) - spamcheck (0.1.0) + spamcheck (1.0.0) grpc (~> 1.0) spring (2.1.1) spring-commands-rspec (1.0.4) @@ -1746,7 +1746,7 @@ DEPENDENCIES slack-messenger (~> 2.3.4) snowplow-tracker (~> 0.6.1) solargraph (~> 0.45.0) - spamcheck (~> 0.1.0) + spamcheck (~> 1.0.0) spring (~> 2.1.0) spring-commands-rspec (~> 1.0.4) sprite-factory (~> 1.7) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index a030797c698..a3ffb4df7b7 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -165,6 +165,7 @@ export class AwardsHandler { `; const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body; + // eslint-disable-next-line no-unsanitized/method targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup); this.addRemainingEmojiMenuCategories(); @@ -198,6 +199,7 @@ export class AwardsHandler { emojisInCategory, ); requestAnimationFrame(() => { + // eslint-disable-next-line no-unsanitized/method emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup); resolve(); }); diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue index 0eb4e6e7709..71560c7de3a 100644 --- a/app/assets/javascripts/batch_comments/components/preview_item.vue +++ b/app/assets/javascripts/batch_comments/components/preview_item.vue @@ -67,6 +67,7 @@ export default { }, content() { const el = document.createElement('div'); + // eslint-disable-next-line no-unsanitized/property el.innerHTML = this.draft.note_html; return el.textContent; diff --git a/app/assets/javascripts/behaviors/copy_code.js b/app/assets/javascripts/behaviors/copy_code.js index 6d2a4c245cc..a653769b60f 100644 --- a/app/assets/javascripts/behaviors/copy_code.js +++ b/app/assets/javascripts/behaviors/copy_code.js @@ -22,6 +22,7 @@ class CopyCodeButton extends HTMLElement { 'data-clipboard-target': `pre#${this.for}`, }); + // eslint-disable-next-line no-unsanitized/property button.innerHTML = spriteIcon('copy-to-clipboard'); return button; diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index af7aac4cf36..ac41af4df7a 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -91,6 +91,7 @@ class SafeMathRenderer { `; if (!wrapperElement.classList.contains('lazy-alert-shown')) { + // eslint-disable-next-line no-unsanitized/property wrapperElement.innerHTML = html; wrapperElement.append(codeElement); wrapperElement.classList.add('lazy-alert-shown'); @@ -111,6 +112,7 @@ class SafeMathRenderer { } try { + // eslint-disable-next-line no-unsanitized/property displayContainer.innerHTML = this.katex.renderToString(text, { displayMode: el.dataset.mathStyle === 'display', throwOnError: true, diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index a0d4f7ef4f2..5ca3f131d99 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -45,6 +45,7 @@ const loadViewer = (viewerParam) => { viewer.dataset.loading = 'true'; return axios.get(url).then(({ data }) => { + // eslint-disable-next-line no-unsanitized/property viewer.innerHTML = data.html; window.requestIdleCallback(() => { diff --git a/app/assets/javascripts/code_navigation/utils/dom_utils.js b/app/assets/javascripts/code_navigation/utils/dom_utils.js index 1a65c1a64a2..90af31b715c 100644 --- a/app/assets/javascripts/code_navigation/utils/dom_utils.js +++ b/app/assets/javascripts/code_navigation/utils/dom_utils.js @@ -23,6 +23,7 @@ const wrapTextWithSpan = (el, text) => { const wrapNodes = (text) => { const wrapper = createSpan(); + // eslint-disable-next-line no-unsanitized/property wrapper.innerHTML = wrapSpacesWithSpans(text); wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text)); return wrapper.childNodes; diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js index f10c2d82b61..0f612989bb4 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js @@ -13,6 +13,7 @@ const renderersByType = { }, header(element, data) { element.classList.add('dropdown-header'); + // eslint-disable-next-line no-unsanitized/property element.innerHTML = data.content; return element; @@ -122,6 +123,7 @@ function assignTextToLink(el, data, options) { const text = getLinkText(data, options); if (options.icon || options.highlight) { + // eslint-disable-next-line no-unsanitized/property el.innerHTML = text; } else { el.textContent = text; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index a8670caf5b2..a6781cffaec 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -81,6 +81,7 @@ export default class FilterableList { onFilterSuccess(response, queryData) { if (response.data.html) { + // eslint-disable-next-line no-unsanitized/property this.listHolderElement.innerHTML = response.data.html; } diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js index 5adc074b3ce..aeea66bf51c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_emoji.js +++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js @@ -75,6 +75,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown { const name = valueElement.innerText; const emojiTag = this.glEmojiTag(name); const emojiElement = dropdownItem.querySelector('gl-emoji'); + // eslint-disable-next-line no-unsanitized/property emojiElement.outerHTML = emojiTag; } }); diff --git a/app/assets/javascripts/filtered_search/droplab/drop_down.js b/app/assets/javascripts/filtered_search/droplab/drop_down.js index 398a7b26677..e7edc678773 100644 --- a/app/assets/javascripts/filtered_search/droplab/drop_down.js +++ b/app/assets/javascripts/filtered_search/droplab/drop_down.js @@ -107,7 +107,7 @@ class DropDown { } const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list; - + // eslint-disable-next-line no-unsanitized/property renderableList.innerHTML = children.join(''); const listEvent = new CustomEvent('render.dl', { @@ -121,7 +121,7 @@ class DropDown { renderChildren(data) { const html = utils.template(this.templateString, data); const template = document.createElement('div'); - + // eslint-disable-next-line no-unsanitized/property template.innerHTML = html; DropDown.setImagesSrc(template); template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block'; diff --git a/app/assets/javascripts/filtered_search/droplab/hook_button.js b/app/assets/javascripts/filtered_search/droplab/hook_button.js index c51d6167fa3..805905e7750 100644 --- a/app/assets/javascripts/filtered_search/droplab/hook_button.js +++ b/app/assets/javascripts/filtered_search/droplab/hook_button.js @@ -42,6 +42,7 @@ class HookButton extends Hook { } restoreInitialState() { + // eslint-disable-next-line no-unsanitized/property this.list.list.innerHTML = this.list.initialState; } diff --git a/app/assets/javascripts/filtered_search/droplab/hook_input.js b/app/assets/javascripts/filtered_search/droplab/hook_input.js index c523dae347f..32dfe0372bb 100644 --- a/app/assets/javascripts/filtered_search/droplab/hook_input.js +++ b/app/assets/javascripts/filtered_search/droplab/hook_input.js @@ -97,6 +97,7 @@ class HookInput extends Hook { } restoreInitialState() { + // eslint-disable-next-line no-unsanitized/property this.list.list.innerHTML = this.list.initialState; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 7143cb50ea6..0c01220a7be 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -122,6 +122,7 @@ export default class FilteredSearchVisualTokens { const hasOperator = Boolean(operator); if (value) { + // eslint-disable-next-line no-unsanitized/property li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({ canEdit, uppercaseTokenName, @@ -138,6 +139,7 @@ export default class FilteredSearchVisualTokens { operatorHTML = '