Commit c8cbc1d2 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge remote-tracking branch 'dev/12-10-stable' into 12-10-stable

parents 6af81c11 5c8f65cd
This diff is collapsed.
...@@ -2,6 +2,27 @@ ...@@ -2,6 +2,27 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 12.10.13 (2020-07-01)
### Security (15 changes)
- Do not show activity for users with private profiles.
- Fix stored XSS in markdown renderer.
- Upgrade swagger-ui to solve XSS issues.
- Fix group deploy token API authorizations.
- Check access when sending TODOs related to merge requests.
- Change from hybrid to JSON cookies serializer.
- Prevent XSS in group name validations.
- Disable caching for wiki attachments.
- Fix null byte error in upload path.
- Update permissions for time tracking endpoints.
- Update Kaminari gem.
- Fix note author name rendering.
- Sanitize bitbucket repo urls to mitigate XSS.
- Stored XSS on the Error Tracking page.
- Fix security issue when rendering issuable.
## 12.10.12 (2020-06-24) ## 12.10.12 (2020-06-24)
### Fixed (1 change) ### Fixed (1 change)
......
...@@ -563,19 +563,19 @@ GEM ...@@ -563,19 +563,19 @@ GEM
json-schema (2.8.0) json-schema (2.8.0)
addressable (>= 2.4) addressable (>= 2.4)
jwt (2.1.0) jwt (2.1.0)
kaminari (1.0.1) kaminari (1.2.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.0.1) kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.0.1) kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.0.1) kaminari-core (= 1.2.1)
kaminari-actionview (1.0.1) kaminari-actionview (1.2.1)
actionview actionview
kaminari-core (= 1.0.1) kaminari-core (= 1.2.1)
kaminari-activerecord (1.0.1) kaminari-activerecord (1.2.1)
activerecord activerecord
kaminari-core (= 1.0.1) kaminari-core (= 1.2.1)
kaminari-core (1.0.1) kaminari-core (1.2.1)
kgio (2.11.2) kgio (2.11.3)
knapsack (1.17.0) knapsack (1.17.0)
rake rake
kramdown (2.1.0) kramdown (2.1.0)
......
<script> <script>
import { escape as esc } from 'lodash'; import { GlTooltip, GlSprintf } from '@gitlab/ui';
import { GlTooltip } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -11,6 +9,7 @@ export default { ...@@ -11,6 +9,7 @@ export default {
ClipboardButton, ClipboardButton,
FileIcon, FileIcon,
Icon, Icon,
GlSprintf,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
...@@ -57,36 +56,6 @@ export default { ...@@ -57,36 +56,6 @@ export default {
collapseIcon() { collapseIcon() {
return this.isExpanded ? 'chevron-down' : 'chevron-right'; return this.isExpanded ? 'chevron-down' : 'chevron-right';
}, },
errorFnText() {
return this.errorFn
? sprintf(
__(`%{spanStart}in%{spanEnd} %{errorFn}`),
{
errorFn: `<strong>${esc(this.errorFn)}</strong>`,
spanStart: `<span class="text-tertiary">`,
spanEnd: `</span>`,
},
false,
)
: '';
},
errorPositionText() {
return this.errorLine
? sprintf(
__(`%{spanStart}at line%{spanEnd} %{errorLine}%{errorColumn}`),
{
errorLine: `<strong>${this.errorLine}</strong>`,
errorColumn: this.errorColumn ? `:<strong>${this.errorColumn}</strong>` : ``,
spanStart: `<span class="text-tertiary">`,
spanEnd: `</span>`,
},
false,
)
: '';
},
errorInfo() {
return `${this.errorFnText} ${this.errorPositionText}`;
},
}, },
methods: { methods: {
isHighlighted(lineNum) { isHighlighted(lineNum) {
...@@ -132,7 +101,27 @@ export default { ...@@ -132,7 +101,27 @@ export default {
:text="filePath" :text="filePath"
css-class="btn-default btn-transparent btn-clipboard position-static" css-class="btn-default btn-transparent btn-clipboard position-static"
/> />
<span v-html="errorInfo"></span>
<gl-sprintf v-if="errorFn" :message="__('%{spanStart}in%{spanEnd} %{errorFn}')">
<template #span="{content}">
<span class="gl-text-gray-400">{{ content }}&nbsp;</span>
</template>
<template #errorFn>
<strong>{{ errorFn }}&nbsp;</strong>
</template>
</gl-sprintf>
<gl-sprintf :message="__('%{spanStart}at line%{spanEnd} %{errorLine}%{errorColumn}')">
<template #span="{content}">
<span class="gl-text-gray-400">{{ content }}&nbsp;</span>
</template>
<template #errorLine>
<strong>{{ errorLine }}</strong>
</template>
<template #errorColumn>
<strong v-if="errorColumn">:{{ errorColumn }}</strong>
</template>
</gl-sprintf>
</div> </div>
</div> </div>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
* any changes done to the haml need to be reflected here. * any changes done to the haml need to be reflected here.
*/ */
import { escape, isNumber } from 'lodash'; import { escape, isNumber } from 'lodash';
import { GlLink, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { GlLink, GlTooltipDirective as GlTooltip, GlSprintf } from '@gitlab/ui';
import { import {
dateInWords, dateInWords,
formatDate, formatDate,
...@@ -20,10 +20,14 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -20,10 +20,14 @@ import Icon from '~/vue_shared/components/icon.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
export default { export default {
i18n: {
openedAgo: __('opened %{timeAgoString} by %{user}'),
},
components: { components: {
Icon, Icon,
IssueAssignees, IssueAssignees,
GlLink, GlLink,
GlSprintf,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
...@@ -98,23 +102,21 @@ export default { ...@@ -98,23 +102,21 @@ export default {
} }
return __('Milestone'); return __('Milestone');
}, },
openedAgoByString() { issuableAuthor() {
const { author, created_at } = this.issuable; return this.issuable.author;
},
issuableCreatedAt() {
return getTimeago().format(this.issuable.created_at);
},
popoverDataAttrs() {
const { id, username, name, avatar_url } = this.issuableAuthor;
return sprintf( return {
__('opened %{timeAgoString} by %{user}'), 'data-user-id': id,
{ 'data-username': username,
timeAgoString: escape(getTimeago().format(created_at)), 'data-name': name,
user: `<a href="${escape(author.web_url)}" 'data-avatar-url': avatar_url,
data-user-id=${escape(author.id)} };
data-username=${escape(author.username)}
data-name=${escape(author.name)}
data-avatar-url="${escape(author.avatar_url)}">
${escape(author.name)}
</a>`,
},
false,
);
}, },
referencePath() { referencePath() {
return this.issuable.references.relative; return this.issuable.references.relative;
...@@ -160,7 +162,7 @@ export default { ...@@ -160,7 +162,7 @@ export default {
mounted() { mounted() {
// TODO: Refactor user popover to use its own component instead of // TODO: Refactor user popover to use its own component instead of
// spawning event listeners on Vue-rendered elements. // spawning event listeners on Vue-rendered elements.
initUserPopovers([this.$refs.openedAgoByContainer.querySelector('a')]); initUserPopovers([this.$refs.openedAgoByContainer.$el]);
}, },
methods: { methods: {
labelStyle(label) { labelStyle(label) {
...@@ -221,17 +223,30 @@ export default { ...@@ -221,17 +223,30 @@ export default {
></i> ></i>
<gl-link :href="issuable.web_url">{{ issuable.title }}</gl-link> <gl-link :href="issuable.web_url">{{ issuable.title }}</gl-link>
</span> </span>
<span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block">{{ <span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block">
issuable.task_status {{ issuable.task_status }}
}}</span> </span>
</div> </div>
<div class="issuable-info"> <div class="issuable-info">
<span class="js-ref-path">{{ referencePath }}</span> <span class="js-ref-path">{{ referencePath }}</span>
<span class="d-none d-sm-inline-block mr-1"> <span data-testid="openedByMessage" class="d-none d-sm-inline-block mr-1">
&middot; &middot;
<span ref="openedAgoByContainer" v-html="openedAgoByString"></span> <gl-sprintf :message="$options.i18n.openedAgo">
<template #timeAgoString>
<span>{{ issuableCreatedAt }}</span>
</template>
<template #user>
<gl-link
ref="openedAgoByContainer"
v-bind="popoverDataAttrs"
:href="issuableAuthor.web_url"
>
{{ issuableAuthor.name }}
</gl-link>
</template>
</gl-sprintf>
</span> </span>
<gl-link <gl-link
......
...@@ -34,6 +34,18 @@ class Groups::ApplicationController < ApplicationController ...@@ -34,6 +34,18 @@ class Groups::ApplicationController < ApplicationController
end end
end end
def authorize_create_deploy_token!
unless can?(current_user, :create_deploy_token, group)
return render_404
end
end
def authorize_destroy_deploy_token!
unless can?(current_user, :destroy_deploy_token, group)
return render_404
end
end
def authorize_admin_group_member! def authorize_admin_group_member!
unless can?(current_user, :admin_group_member, group) unless can?(current_user, :admin_group_member, group)
return render_403 return render_403
......
# frozen_string_literal: true # frozen_string_literal: true
class Groups::DeployTokensController < Groups::ApplicationController class Groups::DeployTokensController < Groups::ApplicationController
before_action :authorize_admin_group! before_action :authorize_destroy_deploy_token!
def revoke def revoke
@token = @group.deploy_tokens.find(params[:id]) @token = @group.deploy_tokens.find(params[:id])
......
...@@ -4,7 +4,7 @@ module Groups ...@@ -4,7 +4,7 @@ module Groups
module Settings module Settings
class RepositoryController < Groups::ApplicationController class RepositoryController < Groups::ApplicationController
skip_cross_project_access_check :show skip_cross_project_access_check :show
before_action :authorize_admin_group! before_action :authorize_create_deploy_token!
before_action :define_deploy_token_variables before_action :define_deploy_token_variables
before_action do before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @group) push_frontend_feature_flag(:ajax_new_deploy_token, @group)
......
...@@ -45,7 +45,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -45,7 +45,7 @@ class Projects::WikisController < Projects::ApplicationController
render 'show' render 'show'
elsif file_blob elsif file_blob
send_blob(@project_wiki.repository, file_blob, allow_caching: @project.public?) send_blob(@project_wiki.repository, file_blob)
elsif show_create_form? elsif show_create_form?
# Assign a title to the WikiPage unless `id` is a randomly generated slug from #new # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new
title = params[:id] unless params[:random_title].present? title = params[:id] unless params[:random_title].present?
......
...@@ -33,6 +33,8 @@ class EventsFinder ...@@ -33,6 +33,8 @@ class EventsFinder
end end
def execute def execute
return Event.none if cannot_access_private_profile?
events = get_events events = get_events
events = by_current_user_access(events) events = by_current_user_access(events)
...@@ -102,6 +104,10 @@ class EventsFinder ...@@ -102,6 +104,10 @@ class EventsFinder
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def cannot_access_private_profile?
source.is_a?(User) && !Ability.allowed?(current_user, :read_user_profile, source)
end
def sort(events) def sort(events)
return events unless params[:sort] return events unless params[:sort]
......
...@@ -70,9 +70,12 @@ class Group < Namespace ...@@ -70,9 +70,12 @@ class Group < Namespace
validates :variables, variable_duplicates: true validates :variables, variable_duplicates: true
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :name, validates :name,
format: { with: Gitlab::Regex.group_name_regex, html_safety: true,
message: Gitlab::Regex.group_name_regex_message }, if: :name_changed? format: { with: Gitlab::Regex.group_name_regex,
message: Gitlab::Regex.group_name_regex_message },
if: :name_changed?
add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required } add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
......
...@@ -513,7 +513,7 @@ class MergeRequest < ApplicationRecord ...@@ -513,7 +513,7 @@ class MergeRequest < ApplicationRecord
participants << merge_user participants << merge_user
end end
participants participants.select { |participant| Ability.allowed?(participant, :read_merge_request, self) }
end end
def first_commit def first_commit
......
...@@ -109,9 +109,7 @@ class GroupPolicy < BasePolicy ...@@ -109,9 +109,7 @@ class GroupPolicy < BasePolicy
enable :create_cluster enable :create_cluster
enable :update_cluster enable :update_cluster
enable :admin_cluster enable :admin_cluster
enable :destroy_deploy_token
enable :read_deploy_token enable :read_deploy_token
enable :create_deploy_token
enable :admin_wiki enable :admin_wiki
end end
...@@ -123,6 +121,8 @@ class GroupPolicy < BasePolicy ...@@ -123,6 +121,8 @@ class GroupPolicy < BasePolicy
enable :set_note_created_at enable :set_note_created_at
enable :set_emails_disabled enable :set_emails_disabled
enable :create_deploy_token
enable :destroy_deploy_token
end end
rule { can?(:read_nested_project_resources) }.policy do rule { can?(:read_nested_project_resources) }.policy do
......
# frozen_string_literal: true
# HtmlSafetyValidator
#
# Validates that a value does not contain HTML
# or other unsafe content that could lead to XSS.
# Relies on Rails HTML Sanitizer:
# https://github.com/rails/rails-html-sanitizer
#
# Example:
#
# class Group < ActiveRecord::Base
# validates :name, presence: true, html_safety: true
# end
#
class HtmlSafetyValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank? || safe_value?(value)
record.errors.add(attribute, self.class.error_message)
end
def self.error_message
_("cannot contain HTML/XML tags, including any word between angle brackets (<,>).")
end
private
# The `FullSanitizer` encodes ampersands as the HTML entity name.
# This isn't particularly necessary for preventing XSS so the ampersand
# is pre-encoded to avoid it being flagged in the comparison.
def safe_value?(text)
pre_encoded_text = text.gsub('&', '&amp;')
Rails::Html::FullSanitizer.new.sanitize(pre_encoded_text) == pre_encoded_text
end
end
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
- @repos.each do |repo| - @repos.each do |repo|
%tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { project: repo.project_key, repository: repo.slug } } %tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { project: repo.project_key, repository: repo.slug } }
%td %td
= link_to repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer' = sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel))
%td.import-target %td.import-target
%fieldset.row %fieldset.row
.input-group .input-group
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
- @incompatible_repos.each do |repo| - @incompatible_repos.each do |repo|
%tr{ id: "repo_#{repo.project_key}___#{repo.slug}" } %tr{ id: "repo_#{repo.project_key}___#{repo.slug}" }
%td %td
= link_to repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer' = sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel))
%td.import-target %td.import-target
%td.import-actions-job-status %td.import-actions-job-status
= label_tag 'Incompatible Project', nil, class: 'label badge-danger' = label_tag 'Incompatible Project', nil, class: 'label badge-danger'
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
.note-header-info .note-header-info
%a{ href: user_path(note.author) } %a{ href: user_path(note.author) }
%span.note-header-author-name.bold %span.note-header-author-name.bold
= sanitize(note.author.name) = note.author.name
= user_status(note.author) = user_status(note.author)
%span.note-headline-light %span.note-headline-light
= note.author.to_reference = note.author.to_reference
......
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
Rails.application.config.action_dispatch.use_cookies_with_metadata = false Rails.application.config.action_dispatch.use_cookies_with_metadata = false
Rails.application.config.action_dispatch.cookies_serializer = :hybrid Rails.application.config.action_dispatch.cookies_serializer =
Gitlab::Utils.to_boolean(ENV['USE_UNSAFE_HYBRID_COOKIES']) ? :hybrid : :json
...@@ -136,7 +136,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git ...@@ -136,7 +136,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
## Group deploy tokens ## Group deploy tokens
These endpoints require group maintainer access or higher. Group maintainers and owners can list group deploy
tokens. Only group owners can create and delete group deploy tokens.
### List group deploy tokens ### List group deploy tokens
......
...@@ -239,6 +239,8 @@ group. ...@@ -239,6 +239,8 @@ group.
| Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) | | Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) |
| Edit group | | | | | ✓ | | Edit group | | | | | ✓ |
| Manage group level CI/CD variables | | | | | ✓ | | Manage group level CI/CD variables | | | | | ✓ |
| List group deploy tokens | | | | ✓ | ✓ |
| Create/Delete group deploy tokens | | | | | ✓ |
| Manage group members | | | | | ✓ | | Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ | | Remove group | | | | | ✓ |
| Delete group epic **(ULTIMATE)** | | | | | ✓ | | Delete group epic **(ULTIMATE)** | | | | | ✓ |
......
...@@ -14,8 +14,8 @@ module API ...@@ -14,8 +14,8 @@ module API
"#{issuable_name}_iid".to_sym "#{issuable_name}_iid".to_sym
end end
def update_issuable_key def admin_issuable_key
"update_#{issuable_name}".to_sym "admin_#{issuable_name}".to_sym
end end
def read_issuable_key def read_issuable_key
...@@ -60,7 +60,7 @@ module API ...@@ -60,7 +60,7 @@ module API
requires :duration, type: String, desc: 'The duration to be parsed' requires :duration, type: String, desc: 'The duration to be parsed'
end end
post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
authorize! update_issuable_key, load_issuable authorize! admin_issuable_key, load_issuable
status :ok status :ok
update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration))) update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
...@@ -71,7 +71,7 @@ module API ...@@ -71,7 +71,7 @@ module API
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end end
post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
authorize! update_issuable_key, load_issuable authorize! admin_issuable_key, load_issuable
status :ok status :ok
update_issuable(time_estimate: 0) update_issuable(time_estimate: 0)
...@@ -83,7 +83,7 @@ module API ...@@ -83,7 +83,7 @@ module API
requires :duration, type: String, desc: 'The duration to be parsed' requires :duration, type: String, desc: 'The duration to be parsed'
end end
post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
authorize! update_issuable_key, load_issuable authorize! admin_issuable_key, load_issuable
update_issuable(spend_time: { update_issuable(spend_time: {
duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)), duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
...@@ -96,7 +96,7 @@ module API ...@@ -96,7 +96,7 @@ module API
requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
end end
post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
authorize! update_issuable_key, load_issuable authorize! admin_issuable_key, load_issuable
status :ok status :ok
update_issuable(spend_time: { duration: :reset, user_id: current_user.id }) update_issuable(spend_time: { duration: :reset, user_id: current_user.id })
......
...@@ -253,7 +253,7 @@ module Banzai ...@@ -253,7 +253,7 @@ module Banzai
object_parent_type = parent.is_a?(Group) ? :group : :project object_parent_type = parent.is_a?(Group) ? :group : :project
{ {
original: text, original: escape_html_entities(text),
link: link_content, link: link_content,
link_reference: link_reference, link_reference: link_reference,
object_parent_type => parent.id, object_parent_type => parent.id,
......
...@@ -38,7 +38,7 @@ module Banzai ...@@ -38,7 +38,7 @@ module Banzai
private private
def unescape_and_scrub_uri(uri) def unescape_and_scrub_uri(uri)
Addressable::URI.unescape(uri).scrub Addressable::URI.unescape(uri).scrub.delete("\0")
end end
end end
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Gitlab module Gitlab
module MarkdownCache module MarkdownCache
# Increment this number every time the renderer changes its output # Increment this number every time the renderer changes its output
CACHE_COMMONMARK_VERSION = 20 CACHE_COMMONMARK_VERSION = 23
CACHE_COMMONMARK_VERSION_START = 10 CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError) BaseError = Class.new(StandardError)
......
...@@ -24308,6 +24308,9 @@ msgstr "" ...@@ -24308,6 +24308,9 @@ msgstr ""
msgid "cannot block others" msgid "cannot block others"
msgstr "" msgstr ""
msgid "cannot contain HTML/XML tags, including any word between angle brackets (<,>)."
msgstr ""
msgid "cannot include leading slash or directory traversal." msgid "cannot include leading slash or directory traversal."
msgstr "" msgstr ""
......
...@@ -141,43 +141,19 @@ describe Projects::WikisController do ...@@ -141,43 +141,19 @@ describe Projects::WikisController do
context 'when page is a file' do context 'when page is a file' do
include WikiHelpers include WikiHelpers
let(:id) { upload_file_to_wiki(project, user, file_name) } where(:file_name) { ['dk.png', 'unsanitized.svg', 'git-cheat-sheet.pdf'] }
context 'when file is an image' do with_them do
let(:file_name) { 'dk.png' } let(:id) { upload_file_to_wiki(project, user, file_name) }
it 'delivers the image' do it 'delivers the file with the correct headers' do
subject subject
expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq('true')
expect(response.cache_control[:public]).to be(false)
expect(response.cache_control[:extras]).to include('no-store')
end end
context 'when file is a svg' do
let(:file_name) { 'unsanitized.svg' }
it 'delivers the image' do
subject
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
end
it_behaves_like 'project cache control headers'
end
context 'when file is a pdf' do
let(:file_name) { 'git-cheat-sheet.pdf' }
it 'sets the content type to sets the content response headers' do
subject
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
it_behaves_like 'project cache control headers'
end end
end end
end end
......
...@@ -5,15 +5,17 @@ require 'spec_helper' ...@@ -5,15 +5,17 @@ require 'spec_helper'
describe 'Comments on personal snippets', :js do describe 'Comments on personal snippets', :js do
include NoteInteractionHelpers include NoteInteractionHelpers
let!(:user) { create(:user) } let_it_be(:snippet) { create(:personal_snippet, :public) }
let!(:snippet) { create(:personal_snippet, :public) } let_it_be(:other_note) { create(:note_on_personal_snippet) }
let(:user_name) { 'Test User' }
let!(:user) { create(:user, name: user_name) }
let!(:snippet_notes) do let!(:snippet_notes) do
[ [
create(:note_on_personal_snippet, noteable: snippet, author: user), create(:note_on_personal_snippet, noteable: snippet, author: user),
create(:note_on_personal_snippet, noteable: snippet) create(:note_on_personal_snippet, noteable: snippet)
] ]
end end
let!(:other_note) { create(:note_on_personal_snippet) }
before do before do
stub_feature_flags(snippets_vue: false) stub_feature_flags(snippets_vue: false)
...@@ -56,6 +58,26 @@ describe 'Comments on personal snippets', :js do ...@@ -56,6 +58,26 @@ describe 'Comments on personal snippets', :js do
expect(page).to show_user_status(status) expect(page).to show_user_status(status)
end end
end end
it 'shows the author name' do
visit snippet_path(snippet)
within("#note_#{snippet_notes[0].id}") do
expect(page).to have_content(user_name)
end
end
context 'when the author name contains HTML' do
let(:user_name) { '<h1><a href="https://bad.link/malicious.exe" class="evil">Fake Content<img class="fake-icon" src="image.png"></a></h1>' }
it 'renders the name as plain text' do
visit snippet_path(snippet)
content = find("#note_#{snippet_notes[0].id} .note-header-author-name").text
expect(content).to eq user_name
end
end
end end
context 'when submitting a note' do context 'when submitting a note' do
......
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
describe EventsFinder do describe EventsFinder do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:private_user) { create(:user, private_profile: true) }
let(:other_user) { create(:user) } let(:other_user) { create(:user) }
let(:project1) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } let(:project1) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
...@@ -57,6 +58,12 @@ describe EventsFinder do ...@@ -57,6 +58,12 @@ describe EventsFinder do
expect(events).to be_empty expect(events).to be_empty
end end
it 'returns nothing when the target profile is private' do
events = described_class.new(source: private_user, current_user: other_user).execute
expect(events).to be_empty
end
end end
describe 'wiki events feature flag' do describe 'wiki events feature flag' do
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue'; import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { trimText } from 'helpers/text_helper';
describe('Stacktrace Entry', () => { describe('Stacktrace Entry', () => {
let wrapper; let wrapper;
...@@ -21,6 +23,9 @@ describe('Stacktrace Entry', () => { ...@@ -21,6 +23,9 @@ describe('Stacktrace Entry', () => {
errorLine: 24, errorLine: 24,
...props, ...props,
}, },
stubs: {
GlSprintf,
},
}); });
} }
...@@ -53,7 +58,7 @@ describe('Stacktrace Entry', () => { ...@@ -53,7 +58,7 @@ describe('Stacktrace Entry', () => {
const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: 77 }; const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: 77 };
mountComponent({ expanded: false, lines: [], ...extraInfo }); mountComponent({ expanded: false, lines: [], ...extraInfo });
expect(wrapper.find(Icon).exists()).toBe(false); expect(wrapper.find(Icon).exists()).toBe(false);
expect(findFileHeaderContent()).toContain( expect(trimText(findFileHeaderContent())).toContain(
`in ${extraInfo.errorFn} at line ${extraInfo.errorLine}:${extraInfo.errorColumn}`, `in ${extraInfo.errorFn} at line ${extraInfo.errorLine}:${extraInfo.errorColumn}`,
); );
}); });
...@@ -61,17 +66,17 @@ describe('Stacktrace Entry', () => { ...@@ -61,17 +66,17 @@ describe('Stacktrace Entry', () => {
it('should render only lineNo:columnNO when there is no errorFn ', () => { it('should render only lineNo:columnNO when there is no errorFn ', () => {
const extraInfo = { errorLine: 34, errorFn: null, errorColumn: 77 }; const extraInfo = { errorLine: 34, errorFn: null, errorColumn: 77 };
mountComponent({ expanded: false, lines: [], ...extraInfo }); mountComponent({ expanded: false, lines: [], ...extraInfo });
expect(findFileHeaderContent()).not.toContain(`in ${extraInfo.errorFn}`); const fileHeaderContent = trimText(findFileHeaderContent());
expect(findFileHeaderContent()).toContain(`${extraInfo.errorLine}:${extraInfo.errorColumn}`); expect(fileHeaderContent).not.toContain(`in ${extraInfo.errorFn}`);
expect(fileHeaderContent).toContain(`${extraInfo.errorLine}:${extraInfo.errorColumn}`);
}); });
it('should render only lineNo when there is no errorColumn ', () => { it('should render only lineNo when there is no errorColumn ', () => {
const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: null }; const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: null };
mountComponent({ expanded: false, lines: [], ...extraInfo }); mountComponent({ expanded: false, lines: [], ...extraInfo });
expect(findFileHeaderContent()).toContain( const fileHeaderContent = trimText(findFileHeaderContent());
`in ${extraInfo.errorFn} at line ${extraInfo.errorLine}`, expect(fileHeaderContent).toContain(`in ${extraInfo.errorFn} at line ${extraInfo.errorLine}`);
); expect(fileHeaderContent).not.toContain(`:${extraInfo.errorColumn}`);
expect(findFileHeaderContent()).not.toContain(`:${extraInfo.errorColumn}`);
}); });
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
...@@ -44,6 +44,10 @@ describe('Issuable component', () => { ...@@ -44,6 +44,10 @@ describe('Issuable component', () => {
baseUrl: TEST_BASE_URL, baseUrl: TEST_BASE_URL,
...props, ...props,
}, },
stubs: {
'gl-sprintf': GlSprintf,
'gl-link': '<a><slot></slot></a>',
},
}); });
}; };
...@@ -66,12 +70,12 @@ describe('Issuable component', () => { ...@@ -66,12 +70,12 @@ describe('Issuable component', () => {
const findConfidentialIcon = () => wrapper.find('.fa-eye-slash'); const findConfidentialIcon = () => wrapper.find('.fa-eye-slash');
const findTaskStatus = () => wrapper.find('.task-status'); const findTaskStatus = () => wrapper.find('.task-status');
const findOpenedAgoContainer = () => wrapper.find({ ref: 'openedAgoByContainer' }); const findOpenedAgoContainer = () => wrapper.find('[data-testid="openedByMessage"]');
const findMilestone = () => wrapper.find('.js-milestone'); const findMilestone = () => wrapper.find('.js-milestone');
const findMilestoneTooltip = () => findMilestone().attributes('title'); const findMilestoneTooltip = () => findMilestone().attributes('title');
const findDueDate = () => wrapper.find('.js-due-date'); const findDueDate = () => wrapper.find('.js-due-date');
const findLabelContainer = () => wrapper.find('.js-labels'); const findLabelContainer = () => wrapper.find('.js-labels');
const findLabelLinks = () => findLabelContainer().findAll(GlLink); const findLabelLinks = () => findLabelContainer().findAll('a');
const findWeight = () => wrapper.find('.js-weight'); const findWeight = () => wrapper.find('.js-weight');
const findAssignees = () => wrapper.find(IssueAssignees); const findAssignees = () => wrapper.find(IssueAssignees);
const findMergeRequestsCount = () => wrapper.find('.js-merge-requests'); const findMergeRequestsCount = () => wrapper.find('.js-merge-requests');
...@@ -86,7 +90,7 @@ describe('Issuable component', () => { ...@@ -86,7 +90,7 @@ describe('Issuable component', () => {
factory(); factory();
expect(initUserPopovers).toHaveBeenCalledWith([findOpenedAgoContainer().find('a').element]); expect(initUserPopovers).toHaveBeenCalledWith([wrapper.vm.$refs.openedAgoByContainer.$el]);
}); });
}); });
...@@ -135,7 +139,7 @@ describe('Issuable component', () => { ...@@ -135,7 +139,7 @@ describe('Issuable component', () => {
}); });
it('renders fuzzy opened date and author', () => { it('renders fuzzy opened date and author', () => {
expect(trimText(findOpenedAgoContainer().text())).toEqual( expect(trimText(findOpenedAgoContainer().text())).toContain(
`opened 1 month ago by ${TEST_USER_NAME}`, `opened 1 month ago by ${TEST_USER_NAME}`,
); );
}); });
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Cookies serializer initializer' do
def load_initializer
load Rails.root.join('config/initializers/cookies_serializer.rb')
end
subject { Rails.application.config.action_dispatch.cookies_serializer }
it 'uses JSON serializer by default' do
load_initializer
expect(subject).to eq(:json)
end
it 'uses the unsafe hybrid serializer when the environment variables is set' do
stub_env('USE_UNSAFE_HYBRID_COOKIES', 'true')
load_initializer
expect(subject).to eq(:hybrid)
end
end
...@@ -20,6 +20,18 @@ describe Banzai::Filter::AbstractReferenceFilter do ...@@ -20,6 +20,18 @@ describe Banzai::Filter::AbstractReferenceFilter do
end end
end end
describe '#data_attributes_for' do
let_it_be(:issue) { create(:issue, project: project) }
it 'is not an XSS vector' do
allow(described_class).to receive(:object_class).and_return(Issue)
data_attributes = filter.data_attributes_for('xss &lt;img onerror=alert(1) src=x&gt;', project, issue, link_content: true)
expect(data_attributes[:original]).to eq('xss &amp;lt;img onerror=alert(1) src=x&amp;gt;')
end
end
describe '#parent_per_reference' do describe '#parent_per_reference' do
it 'returns a Hash containing projects grouped per parent paths' do it 'returns a Hash containing projects grouped per parent paths' do
expect(filter).to receive(:references_per_parent) expect(filter).to receive(:references_per_parent)
......
...@@ -212,6 +212,7 @@ describe Banzai::Filter::UploadLinkFilter do ...@@ -212,6 +212,7 @@ describe Banzai::Filter::UploadLinkFilter do
'invalid UTF-8 byte sequences' | '%FF' 'invalid UTF-8 byte sequences' | '%FF'
'garbled path' | 'open(/var/tmp/):%20/location%0Afrom:%20/test' 'garbled path' | 'open(/var/tmp/):%20/location%0Afrom:%20/test'
'whitespace' | "d18213acd3732630991986120e167e3d/Landscape_8.jpg\nand more" 'whitespace' | "d18213acd3732630991986120e167e3d/Landscape_8.jpg\nand more"
'null byte' | "%00"
end end
with_them do with_them do
......
...@@ -24,7 +24,7 @@ describe Banzai::Pipeline::FullPipeline do ...@@ -24,7 +24,7 @@ describe Banzai::Pipeline::FullPipeline do
it 'escapes the data-original attribute on a reference' do it 'escapes the data-original attribute on a reference' do
markdown = %Q{[">bad things](#{issue.to_reference})} markdown = %Q{[">bad things](#{issue.to_reference})}
result = described_class.to_html(markdown, project: project) result = described_class.to_html(markdown, project: project)
expect(result).to include(%{data-original='\"&gt;bad things'}) expect(result).to include(%{data-original='\"&amp;gt;bad things'})
end end
end end
......
...@@ -3458,7 +3458,7 @@ describe MergeRequest do ...@@ -3458,7 +3458,7 @@ describe MergeRequest do
describe '#merge_participants' do describe '#merge_participants' do
it 'contains author' do it 'contains author' do
expect(subject.merge_participants).to eq([subject.author]) expect(subject.merge_participants).to contain_exactly(subject.author)
end end
describe 'when merge_when_pipeline_succeeds? is true' do describe 'when merge_when_pipeline_succeeds? is true' do
...@@ -3472,8 +3472,20 @@ describe MergeRequest do ...@@ -3472,8 +3472,20 @@ describe MergeRequest do
author: user) author: user)
end end
it 'contains author only' do context 'author is not a project member' do
expect(subject.merge_participants).to eq([subject.author]) it 'is empty' do
expect(subject.merge_participants).to be_empty
end
end
context 'author is a project member' do
before do
subject.project.team.add_reporter(user)
end
it 'contains author only' do
expect(subject.merge_participants).to contain_exactly(subject.author)
end
end end
end end
...@@ -3486,8 +3498,24 @@ describe MergeRequest do ...@@ -3486,8 +3498,24 @@ describe MergeRequest do
merge_user: merge_user) merge_user: merge_user)
end end
it 'contains author and merge user' do before do
expect(subject.merge_participants).to eq([subject.author, merge_user]) subject.project.team.add_reporter(subject.author)
end
context 'merge user is not a member' do
it 'contains author only' do
expect(subject.merge_participants).to contain_exactly(subject.author)
end
end
context 'both author and merge users are project members' do
before do
subject.project.team.add_reporter(merge_user)
end
it 'contains author and merge user' do
expect(subject.merge_participants).to contain_exactly(subject.author, merge_user)
end
end end
end end
end end
......
...@@ -204,7 +204,7 @@ describe API::DeployTokens do ...@@ -204,7 +204,7 @@ describe API::DeployTokens do
end end
context 'deploy token creation' do context 'deploy token creation' do
shared_examples 'creating a deploy token' do |entity, unauthenticated_response| shared_examples 'creating a deploy token' do |entity, unauthenticated_response, authorized_role|
let(:expires_time) { 1.year.from_now } let(:expires_time) { 1.year.from_now }
let(:params) do let(:params) do
{ {
...@@ -231,9 +231,9 @@ describe API::DeployTokens do ...@@ -231,9 +231,9 @@ describe API::DeployTokens do
it { is_expected.to have_gitlab_http_status(:forbidden) } it { is_expected.to have_gitlab_http_status(:forbidden) }
end end
context 'when authenticated as maintainer' do context "when authenticated as #{authorized_role}" do
before do before do
send(entity).add_maintainer(user) send(entity).send("add_#{authorized_role}", user)
end end
it 'creates the deploy token' do it 'creates the deploy token' do
...@@ -282,7 +282,7 @@ describe API::DeployTokens do ...@@ -282,7 +282,7 @@ describe API::DeployTokens do
response response
end end
it_behaves_like 'creating a deploy token', :project, :not_found it_behaves_like 'creating a deploy token', :project, :not_found, :maintainer
end end
describe 'POST /groups/:id/deploy_tokens' do describe 'POST /groups/:id/deploy_tokens' do
...@@ -291,7 +291,17 @@ describe API::DeployTokens do ...@@ -291,7 +291,17 @@ describe API::DeployTokens do
response response
end end
it_behaves_like 'creating a deploy token', :group, :forbidden it_behaves_like 'creating a deploy token', :group, :forbidden, :owner
context 'when authenticated as maintainer' do
before do
group.add_maintainer(user)
end
let(:params) { { name: 'test', scopes: ['read_repository'] } }
it { is_expected.to have_gitlab_http_status(:forbidden) }
end
end end
end end
...@@ -320,6 +330,14 @@ describe API::DeployTokens do ...@@ -320,6 +330,14 @@ describe API::DeployTokens do
group.add_maintainer(user) group.add_maintainer(user)
end end
it { is_expected.to have_gitlab_http_status(:forbidden) }
end
context 'when authenticated as owner' do
before do
group.add_owner(user)
end
it 'calls the deploy token destroy service' do it 'calls the deploy token destroy service' do
expect(::Groups::DeployTokens::DestroyService).to receive(:new) expect(::Groups::DeployTokens::DestroyService).to receive(:new)
.with(group, user, token_id: group_deploy_token.id) .with(group, user, token_id: group_deploy_token.id)
......
...@@ -192,6 +192,19 @@ describe API::Events do ...@@ -192,6 +192,19 @@ describe API::Events do
end end
end end
context 'when target users profile is private' do
it 'returns no events' do
user.update!(private_profile: true)
private_project.add_developer(non_member)
get api("/users/#{user.username}/events", non_member)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to eq([])
end
end
context 'when scope is passed' do context 'when scope is passed' do
context 'when unauthenticated' do context 'when unauthenticated' do
it 'returns no user events' do it 'returns no user events' do
......
...@@ -4,6 +4,16 @@ RSpec.shared_examples 'an unauthorized API user' do ...@@ -4,6 +4,16 @@ RSpec.shared_examples 'an unauthorized API user' do
it { is_expected.to eq(403) } it { is_expected.to eq(403) }
end end
RSpec.shared_examples 'API user with insufficient permissions' do
context 'with non member that is the author' do
before do
issuable.update!(author: non_member) # an external author can't admin issuable
end
it_behaves_like 'an unauthorized API user'
end
end
RSpec.shared_examples 'time tracking endpoints' do |issuable_name| RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
let(:non_member) { create(:user) } let(:non_member) { create(:user) }
...@@ -14,6 +24,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| ...@@ -14,6 +24,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), params: { duration: '1w' }) } subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), params: { duration: '1w' }) }
it_behaves_like 'an unauthorized API user' it_behaves_like 'an unauthorized API user'
it_behaves_like 'API user with insufficient permissions'
end end
it "sets the time estimate for #{issuable_name}" do it "sets the time estimate for #{issuable_name}" do
...@@ -53,6 +64,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| ...@@ -53,6 +64,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", non_member)) } subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", non_member)) }
it_behaves_like 'an unauthorized API user' it_behaves_like 'an unauthorized API user'
it_behaves_like 'API user with insufficient permissions'
end end
it "resets the time estimate for #{issuable_name}" do it "resets the time estimate for #{issuable_name}" do
...@@ -70,6 +82,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| ...@@ -70,6 +82,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
end end
it_behaves_like 'an unauthorized API user' it_behaves_like 'an unauthorized API user'
it_behaves_like 'API user with insufficient permissions'
end end
it "add spent time for #{issuable_name}" do it "add spent time for #{issuable_name}" do
...@@ -119,6 +132,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| ...@@ -119,6 +132,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", non_member)) } subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", non_member)) }
it_behaves_like 'an unauthorized API user' it_behaves_like 'an unauthorized API user'
it_behaves_like 'API user with insufficient permissions'
end end
it "resets spent time for #{issuable_name}" do it "resets spent time for #{issuable_name}" do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe HtmlSafetyValidator do
let(:validator) { described_class.new(attributes: [:name]) }
let(:group) { build(:group) }
def validate(value)
validator.validate_each(group, :name, value)
end
it 'adds an error when a script is included in the name' do
validate('My group <script>evil_script</script>')
expect(group.errors[:name]).to eq([HtmlSafetyValidator.error_message])
end
it 'does not add an error when an ampersand is included in the name' do
validate('Group with 1 & 2')
expect(group.errors).to be_empty
end
end
...@@ -10876,10 +10876,10 @@ svg4everybody@2.1.9: ...@@ -10876,10 +10876,10 @@ svg4everybody@2.1.9:
resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d" resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d"
integrity sha1-W9n23vwTOFmgRGRtR0P6vCjbfi0= integrity sha1-W9n23vwTOFmgRGRtR0P6vCjbfi0=
swagger-ui-dist@^3.24.3: swagger-ui-dist@^3.26.2:
version "3.24.3" version "3.26.2"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.24.3.tgz#99754d11b0ddd314a1a50db850acb415e4b0a0c6" resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.26.2.tgz#22c700906c8911b1c9956da6c3fca371dba6219f"
integrity sha512-kB8qobP42Xazaym7sD9g5mZuRL4416VIIYZMqPEIskkzKqbPLQGEiHA3ga31bdzyzFLgr6Z797+6X1Am6zYpbg== integrity sha512-cpR3A9uEs95gGQSaIXgiTpnetIifTF1u2a0fWrnVl+HyLpCdHVgOy7FGlVD1iVkts7AE5GOiGjA7VyDNiRaNgw==
symbol-observable@^1.0.2: symbol-observable@^1.0.2:
version "1.2.0" version "1.2.0"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment