Commit 11e9b7b5 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/security/gitlab@13-1-stable-ee

parent 2b0b97e7
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -560,18 +560,18 @@ GEM ...@@ -560,18 +560,18 @@ 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.3) kgio (2.11.3)
knapsack (1.17.0) knapsack (1.17.0)
rake rake
......
<script> <script>
import { escape } 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>${escape(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)
......
...@@ -73,9 +73,12 @@ class Group < Namespace ...@@ -73,9 +73,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,
html_safety: true,
format: { with: Gitlab::Regex.group_name_regex, format: { with: Gitlab::Regex.group_name_regex,
message: Gitlab::Regex.group_name_regex_message }, if: :name_changed? 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 }
......
...@@ -98,9 +98,7 @@ class GroupPolicy < BasePolicy ...@@ -98,9 +98,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
end end
rule { owner }.policy do rule { owner }.policy do
...@@ -112,6 +110,8 @@ class GroupPolicy < BasePolicy ...@@ -112,6 +110,8 @@ class GroupPolicy < BasePolicy
enable :set_note_created_at enable :set_note_created_at
enable :set_emails_disabled enable :set_emails_disabled
enable :update_default_branch_protection enable :update_default_branch_protection
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
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,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
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,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
......
---
title: Update xterm js dependency to latest stable 3.x version
merge_request:
author:
type: security
---
title: Fix stored XSS in markdown renderer
merge_request:
author:
type: security
---
title: Upgrade swagger-ui to solve XSS issues
merge_request:
author:
type: security
---
title: Fix group deploy token API authorizations
merge_request:
author:
type: security
---
title: Change from hybrid to JSON cookies serializer
merge_request:
author:
type: security
---
title: Prevent XSS in group name validations
merge_request:
author:
type: security
---
title: Disable Github Importer API by settings
merge_request:
author:
type: security
---
title: Fix null byte error in upload path
merge_request:
author:
type: security
---
title: Update permissions for time tracking endpoints
merge_request:
author:
type: security
---
title: Update Kaminari gem
merge_request:
author:
type: security
---
title: Fix note author name rendering
merge_request:
author:
type: security
---
title: Sanitize bitbucket repo urls to mitigate XSS
merge_request:
author:
type: security
---
title: Stored XSS on the Error Tracking page
merge_request:
author:
type: security
---
title: Fix security issue when rendering issuable
merge_request:
author:
type: security
# 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 = true Rails.application.config.action_dispatch.use_cookies_with_metadata = true
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
......
...@@ -254,6 +254,8 @@ group. ...@@ -254,6 +254,8 @@ group.
| Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) | | Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) |
| Edit group settings | | | | | ✓ | | Edit group settings | | | | | ✓ |
| 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 | | | | | ✓ |
| Delete group | | | | | ✓ | | Delete group | | | | | ✓ |
| Delete group epic **(ULTIMATE)** | | | | | ✓ | | Delete group epic **(ULTIMATE)** | | | | | ✓ |
......
...@@ -4,6 +4,10 @@ module API ...@@ -4,6 +4,10 @@ module API
class ImportGithub < Grape::API class ImportGithub < Grape::API
rescue_from Octokit::Unauthorized, with: :provider_unauthorized rescue_from Octokit::Unauthorized, with: :provider_unauthorized
before do
forbidden! unless Gitlab::CurrentSettings.import_sources&.include?('github')
end
helpers do helpers do
def client def client
@client ||= Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options) @client ||= Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options)
......
...@@ -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 = 21 CACHE_COMMONMARK_VERSION = 23
CACHE_COMMONMARK_VERSION_START = 10 CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError) BaseError = Class.new(StandardError)
......
...@@ -26467,6 +26467,9 @@ msgstr "" ...@@ -26467,6 +26467,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 ""
......
...@@ -126,7 +126,7 @@ ...@@ -126,7 +126,7 @@
"string-hash": "1.1.3", "string-hash": "1.1.3",
"style-loader": "^1.1.3", "style-loader": "^1.1.3",
"svg4everybody": "2.1.9", "svg4everybody": "2.1.9",
"swagger-ui-dist": "^3.24.3", "swagger-ui-dist": "^3.26.2",
"three": "^0.84.0", "three": "^0.84.0",
"three-orbit-controls": "^82.1.0", "three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4", "three-stl-loader": "^1.0.4",
...@@ -152,7 +152,7 @@ ...@@ -152,7 +152,7 @@
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
"webpack-stats-plugin": "^0.3.1", "webpack-stats-plugin": "^0.3.1",
"worker-loader": "^2.0.0", "worker-loader": "^2.0.0",
"xterm": "^3.5.0" "xterm": "3.14.5"
}, },
"devDependencies": { "devDependencies": {
"acorn": "^6.3.0", "acorn": "^6.3.0",
......
...@@ -5,15 +5,17 @@ require 'spec_helper' ...@@ -5,15 +5,17 @@ require 'spec_helper'
RSpec.describe 'Comments on personal snippets', :js do RSpec.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 @@ RSpec.describe 'Comments on personal snippets', :js do ...@@ -56,6 +58,26 @@ RSpec.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
......
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)
......
...@@ -229,6 +229,7 @@ describe Banzai::Filter::UploadLinkFilter do ...@@ -229,6 +229,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
......
...@@ -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)
......
...@@ -26,6 +26,18 @@ describe API::ImportGithub do ...@@ -26,6 +26,18 @@ describe API::ImportGithub do
end end
end end
it 'rejects requests when Github Importer is disabled' do
stub_application_setting(import_sources: nil)
post api("/import/github", user), params: {
target_namespace: user.namespace_path,
personal_access_token: token,
repo_id: non_existing_record_id
}
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns 201 response when the project is imported successfully' do it 'returns 201 response when the project is imported successfully' do
allow(Gitlab::LegacyGithubImport::ProjectCreator) allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
......
...@@ -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
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
...@@ -11181,10 +11181,10 @@ svg4everybody@2.1.9: ...@@ -11181,10 +11181,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"
...@@ -12606,10 +12606,10 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: ...@@ -12606,10 +12606,10 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
xterm@^3.5.0: xterm@3.14.5:
version "3.5.0" version "3.14.5"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.5.0.tgz#ba3f464bc5730c9d259ebe62131862224db9ddcc" resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.14.5.tgz#c9d14e48be6873aa46fb429f22f2165557fd2dea"
integrity sha512-IpG3P3gkT0/xDPS0j3igpk92JYlUajaEHk3/EQSUeIRJmPiF2lyham3Xt/GD3o98uOrRluvowjNj0AFeYK+AXQ== integrity sha512-DVmQ8jlEtL+WbBKUZuMxHMBgK/yeIZwkXB81bH+MGaKKnJGYwA+770hzhXPfwEIokK9On9YIFPRleVp/5G7z9g==
y18n@^3.2.1: y18n@^3.2.1:
version "3.2.1" version "3.2.1"
......
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