Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 12.7.3
- No changes.
## 12.7.2 ## 12.7.2
- No changes. - No changes.
... ...
......
...@@ -2,6 +2,36 @@ ...@@ -2,6 +2,36 @@
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.7.4
### Security (1 change)
- Update workhorse to v8.20.0.
## 12.7.3
### Security (17 changes, 1 of them is from the community)
- Fix xss on frequent groups dropdown. !50
- Bump rubyzip to 2.0.0. (Utkarsh Gupta)
- Disable access to last_pipeline in commits API for users without read permissions.
- Add constraint to group dependency proxy endpoint param.
- Limit number of AsciiDoc includes per document.
- Prevent API access for unconfirmed users.
- Enforce permission check when counting activity events.
- Prevent gafana integration token from being displayed as a plain text to other project maintainers, by only displaying a masked version of it. GraphQL api deprecate token field in GrafanaIntegration type.
- Cleanup todos for users from a removed linked group.
- Fix XSS vulnerability on custom project templates form.
- Protect internal CI builds from external overrides.
- ImportExport::ExportService to require admin_project permission.
- Make sure that only system notes where all references are visible to user are exposed in GraphQL API.
- Disable caching of repository/files/:file_path/raw API endpoint.
- Make cross-repository comparisons happen in the source repository.
- Update excon to 0.71.1 to fix CVE-2019-16779.
- Add workhorse request verification to package upload endpoints.
## 12.7.2 ## 12.7.2
   
- No changes. - No changes.
... ...
......
8.19.0 8.20.0
...@@ -65,7 +65,7 @@ gem 'u2f', '~> 0.2.1' ...@@ -65,7 +65,7 @@ gem 'u2f', '~> 0.2.1'
# GitLab Pages # GitLab Pages
gem 'validates_hostname', '~> 1.0.6' gem 'validates_hostname', '~> 1.0.6'
gem 'rubyzip', '~> 1.3.0', require: 'zip' gem 'rubyzip', '~> 2.0.0', require: 'zip'
# GitLab Pages letsencrypt support # GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.2' gem 'acme-client', '~> 2.0.2'
... ...
......
...@@ -262,7 +262,7 @@ GEM ...@@ -262,7 +262,7 @@ GEM
et-orbi (1.2.1) et-orbi (1.2.1)
tzinfo tzinfo
eventmachine (1.2.7) eventmachine (1.2.7)
excon (0.62.0) excon (0.71.1)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
extended-markdown-filter (0.6.0) extended-markdown-filter (0.6.0)
...@@ -944,7 +944,7 @@ GEM ...@@ -944,7 +944,7 @@ GEM
sexp_processor (~> 4.9) sexp_processor (~> 4.9)
rubyntlm (0.6.2) rubyntlm (0.6.2)
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.3.0) rubyzip (2.0.0)
rugged (0.28.4.1) rugged (0.28.4.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.6) sanitize (4.6.6)
...@@ -1343,7 +1343,7 @@ DEPENDENCIES ...@@ -1343,7 +1343,7 @@ DEPENDENCIES
ruby-prof (~> 1.0.0) ruby-prof (~> 1.0.0)
ruby-progressbar ruby-progressbar
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rubyzip (~> 1.3.0) rubyzip (~> 2.0.0)
rugged (~> 0.28) rugged (~> 0.28)
sanitize (~> 4.6) sanitize (~> 4.6)
sassc-rails (~> 2.1.0) sassc-rails (~> 2.1.0)
... ...
......
12.7.2-ee 12.7.4
...@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor'; ...@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import store from '../store/'; import store from '../store/';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants'; import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
import { isMobile, updateExistingFrequentItem } from '../utils'; import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
import FrequentItemsSearchInput from './frequent_items_search_input.vue'; import FrequentItemsSearchInput from './frequent_items_search_input.vue';
import FrequentItemsList from './frequent_items_list.vue'; import FrequentItemsList from './frequent_items_list.vue';
import frequentItemsMixin from './frequent_items_mixin'; import frequentItemsMixin from './frequent_items_mixin';
...@@ -64,7 +64,9 @@ export default { ...@@ -64,7 +64,9 @@ export default {
this.fetchFrequentItems(); this.fetchFrequentItems();
} }
}, },
logItemAccess(storageKey, item) { logItemAccess(storageKey, unsanitizedItem) {
const item = sanitizeItem(unsanitizedItem);
if (!AccessorUtilities.isLocalStorageAccessSafe()) { if (!AccessorUtilities.isLocalStorageAccessSafe()) {
return false; return false;
} }
... ...
......
<script> <script>
import FrequentItemsListItem from './frequent_items_list_item.vue'; import FrequentItemsListItem from './frequent_items_list_item.vue';
import frequentItemsMixin from './frequent_items_mixin'; import frequentItemsMixin from './frequent_items_mixin';
import { sanitizeItem } from '../utils';
export default { export default {
components: { components: {
...@@ -48,6 +49,9 @@ export default { ...@@ -48,6 +49,9 @@ export default {
? this.translations.itemListErrorMessage ? this.translations.itemListErrorMessage
: this.translations.itemListEmptyMessage; : this.translations.itemListEmptyMessage;
}, },
sanitizedItems() {
return this.items.map(sanitizeItem);
},
}, },
}; };
</script> </script>
...@@ -59,7 +63,7 @@ export default { ...@@ -59,7 +63,7 @@ export default {
{{ listEmptyMessage }} {{ listEmptyMessage }}
</li> </li>
<frequent-items-list-item <frequent-items-list-item
v-for="item in items" v-for="item in sanitizedItems"
v-else v-else
:key="item.id" :key="item.id"
:item-id="item.id" :item-id="item.id"
... ...
......
import _ from 'underscore'; import _ from 'underscore';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import sanitize from 'sanitize-html';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants'; import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize()); export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
...@@ -43,3 +44,9 @@ export const updateExistingFrequentItem = (frequentItem, item) => { ...@@ -43,3 +44,9 @@ export const updateExistingFrequentItem = (frequentItem, item) => {
lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn, lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn,
}; };
}; };
export const sanitizeItem = item => ({
...item,
name: sanitize(item.name.toString(), { allowedTags: [] }),
namespace: sanitize(item.namespace.toString(), { allowedTags: [] }),
});
import $ from 'jquery'; import $ from 'jquery';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import Api from './api'; import Api from './api';
import { escape } from 'lodash';
import { normalizeHeaders } from './lib/utils/common_utils'; import { normalizeHeaders } from './lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -75,10 +76,12 @@ const groupsSelect = () => { ...@@ -75,10 +76,12 @@ const groupsSelect = () => {
} }
}, },
formatResult(object) { formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`; return `<div class='group-result'> <div class='group-name'>${escape(
object.full_name,
)}</div> <div class='group-path'>${object.full_path}</div> </div>`;
}, },
formatSelection(object) { formatSelection(object) {
return object.full_name; return escape(object.full_name);
}, },
dropdownCssClass: 'ajax-groups-dropdown select2-infinite', dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results // we do not want to escape markup since we are displaying html in results
... ...
......
...@@ -5,6 +5,7 @@ require 'fogbugz' ...@@ -5,6 +5,7 @@ require 'fogbugz'
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Gitlab::GonHelper include Gitlab::GonHelper
include Gitlab::NoCacheHeaders
include GitlabRoutingHelper include GitlabRoutingHelper
include PageLayoutHelper include PageLayoutHelper
include SafeParamsHelper include SafeParamsHelper
...@@ -55,7 +56,6 @@ class ApplicationController < ActionController::Base ...@@ -55,7 +56,6 @@ class ApplicationController < ActionController::Base
# Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
# concerns due to caching private data. # concerns due to caching private data.
DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store" DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store"
DEFAULT_GITLAB_CONTROL_NO_CACHE = "#{DEFAULT_GITLAB_CACHE_CONTROL}, no-cache"
rescue_from Encoding::CompatibilityError do |exception| rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception) log_exception(exception)
...@@ -247,9 +247,9 @@ class ApplicationController < ActionController::Base ...@@ -247,9 +247,9 @@ class ApplicationController < ActionController::Base
end end
def no_cache_headers def no_cache_headers
headers['Cache-Control'] = DEFAULT_GITLAB_CONTROL_NO_CACHE DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v|
headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility headers[k] = v
headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT' end
end end
def default_headers def default_headers
... ...
......
...@@ -19,7 +19,7 @@ class DashboardController < Dashboard::ApplicationController ...@@ -19,7 +19,7 @@ class DashboardController < Dashboard::ApplicationController
format.json do format.json do
load_events load_events
pager_json("events/_events", @events.count) pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
end end
end end
end end
...@@ -37,6 +37,7 @@ class DashboardController < Dashboard::ApplicationController ...@@ -37,6 +37,7 @@ class DashboardController < Dashboard::ApplicationController
@events = EventCollection @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter) .new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a .to_a
.map(&:present)
Events::RenderService.new(current_user).execute(@events) Events::RenderService.new(current_user).execute(@events)
end end
... ...
......
...@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController ...@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController
format.json do format.json do
load_events load_events
pager_json("events/_events", @events.count) pager_json("events/_events", @events.count { |event| event.visible_to_user?(current_user) })
end end
end end
end end
...@@ -211,6 +211,7 @@ class GroupsController < Groups::ApplicationController ...@@ -211,6 +211,7 @@ class GroupsController < Groups::ApplicationController
@events = EventCollection @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups) .new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups)
.to_a .to_a
.map(&:present)
Events::RenderService Events::RenderService
.new(current_user) .new(current_user)
... ...
......
...@@ -118,7 +118,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -118,7 +118,7 @@ class ProjectsController < Projects::ApplicationController
format.html format.html
format.json do format.json do
load_events load_events
pager_json('events/_events', @events.count) pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
end end
end end
end end
...@@ -343,6 +343,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -343,6 +343,7 @@ class ProjectsController < Projects::ApplicationController
@events = EventCollection @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter) .new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a .to_a
.map(&:present)
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?) Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end end
... ...
......
...@@ -10,14 +10,19 @@ module Types ...@@ -10,14 +10,19 @@ module Types
description: 'Internal ID of the Grafana integration' description: 'Internal ID of the Grafana integration'
field :grafana_url, GraphQL::STRING_TYPE, null: false, field :grafana_url, GraphQL::STRING_TYPE, null: false,
description: 'Url for the Grafana host for the Grafana integration' description: 'Url for the Grafana host for the Grafana integration'
field :token, GraphQL::STRING_TYPE, null: false,
description: 'API token for the Grafana integration'
field :enabled, GraphQL::BOOLEAN_TYPE, null: false, field :enabled, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates whether Grafana integration is enabled' description: 'Indicates whether Grafana integration is enabled'
field :created_at, Types::TimeType, null: false, field :created_at, Types::TimeType, null: false,
description: 'Timestamp of the issue\'s creation' description: 'Timestamp of the issue\'s creation'
field :updated_at, Types::TimeType, null: false, field :updated_at, Types::TimeType, null: false,
description: 'Timestamp of the issue\'s last activity' description: 'Timestamp of the issue\'s last activity'
field :token, GraphQL::STRING_TYPE, null: false,
deprecation_reason: 'Plain text token has been masked for security reasons',
description: 'API token for the Grafana integration. Field is permanently masked.'
def token
object.masked_token
end
end end
end end
...@@ -368,8 +368,8 @@ module ProjectsHelper ...@@ -368,8 +368,8 @@ module ProjectsHelper
@project.grafana_integration&.grafana_url @project.grafana_integration&.grafana_url
end end
def grafana_integration_token def grafana_integration_masked_token
@project.grafana_integration&.token @project.grafana_integration&.masked_token
end end
def grafana_integration_enabled? def grafana_integration_enabled?
... ...
......
# frozen_string_literal: true # frozen_string_literal: true
class GenericCommitStatus < CommitStatus class GenericCommitStatus < CommitStatus
EXTERNAL_STAGE_IDX = 1_000_000
before_validation :set_default_values before_validation :set_default_values
validates :target_url, addressable_url: true, validates :target_url, addressable_url: true,
length: { maximum: 255 }, length: { maximum: 255 },
allow_nil: true allow_nil: true
validate :name_uniqueness_across_types, unless: :importing?
# GitHub compatible API # GitHub compatible API
alias_attribute :context, :name alias_attribute :context, :name
...@@ -13,7 +16,7 @@ class GenericCommitStatus < CommitStatus ...@@ -13,7 +16,7 @@ class GenericCommitStatus < CommitStatus
def set_default_values def set_default_values
self.context ||= 'default' self.context ||= 'default'
self.stage ||= 'external' self.stage ||= 'external'
self.stage_idx ||= 1000000 self.stage_idx ||= EXTERNAL_STAGE_IDX
end end
def tags def tags
...@@ -25,4 +28,14 @@ class GenericCommitStatus < CommitStatus ...@@ -25,4 +28,14 @@ class GenericCommitStatus < CommitStatus
.new(self, current_user) .new(self, current_user)
.fabricate! .fabricate!
end end
private
def name_uniqueness_across_types
return if !pipeline || name.blank?
if pipeline.statuses.by_name(name).where.not(type: type).exists?
errors.add(:name, :taken)
end
end
end end
...@@ -8,11 +8,13 @@ class GrafanaIntegration < ApplicationRecord ...@@ -8,11 +8,13 @@ class GrafanaIntegration < ApplicationRecord
algorithm: 'aes-256-gcm', algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32 key: Settings.attr_encrypted_db_key_base_32
before_validation :check_token_changes
validates :grafana_url, validates :grafana_url,
length: { maximum: 1024 }, length: { maximum: 1024 },
addressable_url: { enforce_sanitization: true, ascii_only: true } addressable_url: { enforce_sanitization: true, ascii_only: true }
validates :token, :project, presence: true validates :encrypted_token, :project, presence: true
validates :enabled, inclusion: { in: [true, false] } validates :enabled, inclusion: { in: [true, false] }
...@@ -23,4 +25,28 @@ class GrafanaIntegration < ApplicationRecord ...@@ -23,4 +25,28 @@ class GrafanaIntegration < ApplicationRecord
@client ||= ::Grafana::Client.new(api_url: grafana_url.chomp('/'), token: token) @client ||= ::Grafana::Client.new(api_url: grafana_url.chomp('/'), token: token)
end end
def masked_token
mask(encrypted_token)
end
def masked_token_was
mask(encrypted_token_was)
end
private
def token
decrypt(:token, encrypted_token)
end
def check_token_changes
return unless [encrypted_token_was, masked_token_was].include?(token)
clear_attribute_changes [:token, :encrypted_token, :encrypted_token_iv]
end
def mask(token)
token&.squish&.gsub(/./, '*')
end
end end
...@@ -545,7 +545,8 @@ class Note < ApplicationRecord ...@@ -545,7 +545,8 @@ class Note < ApplicationRecord
# if they are not equal, then there are private/confidential references as well # if they are not equal, then there are private/confidential references as well
user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count
else else
referenced_mentionables(user).any? refs = all_references(user)
refs.all.any? && refs.stateful_not_visible_counter == 0
end end
end end
... ...
......
...@@ -2341,6 +2341,10 @@ class Project < ApplicationRecord ...@@ -2341,6 +2341,10 @@ class Project < ApplicationRecord
end end
end end
def template_source?
false
end
private private
def closest_namespace_setting(name) def closest_namespace_setting(name)
... ...
......