...@@ -162,6 +162,10 @@ module ApplicationSettingsHelper ...@@ -162,6 +162,10 @@ module ApplicationSettingsHelper
:allow_local_requests_from_hooks_and_services, :allow_local_requests_from_hooks_and_services,
:dns_rebinding_protection_enabled, :dns_rebinding_protection_enabled,
:archive_builds_in_human_readable, :archive_builds_in_human_readable,
:asset_proxy_enabled,
:asset_proxy_secret_key,
:asset_proxy_url,
:asset_proxy_whitelist,
:authorized_keys_enabled, :authorized_keys_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:auto_devops_domain, :auto_devops_domain,
... ...
......
...@@ -16,12 +16,19 @@ class ApplicationSetting < ApplicationRecord ...@@ -16,12 +16,19 @@ class ApplicationSetting < ApplicationRecord
# fix a lot of tests using allow_any_instance_of # fix a lot of tests using allow_any_instance_of
include ApplicationSettingImplementation include ApplicationSettingImplementation
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
insecure_mode: true,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
ignore_column :koding_url ignore_column :koding_url
ignore_column :koding_enabled ignore_column :koding_enabled
...@@ -180,6 +187,17 @@ class ApplicationSetting < ApplicationRecord ...@@ -180,6 +187,17 @@ class ApplicationSetting < ApplicationRecord
allow_nil: true, allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 }
validates :asset_proxy_url,
presence: true,
allow_blank: false,
url: true,
if: :asset_proxy_enabled?
validates :asset_proxy_secret_key,
presence: true,
allow_blank: false,
if: :asset_proxy_enabled?
SUPPORTED_KEY_TYPES.each do |type| SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end end
... ...
......
...@@ -21,8 +21,9 @@ module ApplicationSettingImplementation ...@@ -21,8 +21,9 @@ module ApplicationSettingImplementation
after_sign_up_text: nil, after_sign_up_text: nil,
akismet_enabled: false, akismet_enabled: false,
allow_local_requests_from_hooks_and_services: false, allow_local_requests_from_hooks_and_services: false,
dns_rebinding_protection_enabled: true, asset_proxy_enabled: false,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
commit_email_hostname: default_commit_email_hostname,
container_registry_token_expire_delay: 5, container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days', default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'], default_branch_protection: Settings.gitlab['default_branch_protection'],
...@@ -31,7 +32,9 @@ module ApplicationSettingImplementation ...@@ -31,7 +32,9 @@ module ApplicationSettingImplementation
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'], default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
dns_rebinding_protection_enabled: true,
domain_whitelist: Settings.gitlab['domain_whitelist'], domain_whitelist: Settings.gitlab['domain_whitelist'],
dsa_key_restriction: 0, dsa_key_restriction: 0,
ecdsa_key_restriction: 0, ecdsa_key_restriction: 0,
...@@ -50,6 +53,7 @@ module ApplicationSettingImplementation ...@@ -50,6 +53,7 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200, housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10, housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'], import_sources: Settings.gitlab['import_sources'],
local_markdown_version: 0,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
mirror_available: true, mirror_available: true,
...@@ -61,6 +65,7 @@ module ApplicationSettingImplementation ...@@ -61,6 +65,7 @@ module ApplicationSettingImplementation
plantuml_url: nil, plantuml_url: nil,
polling_interval_multiplier: 1, polling_interval_multiplier: 1,
project_export_enabled: true, project_export_enabled: true,
protected_ci_variables: false,
recaptcha_enabled: false, recaptcha_enabled: false,
repository_checks_enabled: true, repository_checks_enabled: true,
repository_storages: ['default'], repository_storages: ['default'],
...@@ -92,11 +97,7 @@ module ApplicationSettingImplementation ...@@ -92,11 +97,7 @@ module ApplicationSettingImplementation
user_default_external: false, user_default_external: false,
user_default_internal_regex: nil, user_default_internal_regex: nil,
user_show_add_ssh_key_message: true, user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil, usage_stats_set_by_user_id: nil
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
commit_email_hostname: default_commit_email_hostname,
protected_ci_variables: false,
local_markdown_version: 0
} }
end end
...@@ -139,17 +140,20 @@ module ApplicationSettingImplementation ...@@ -139,17 +140,20 @@ module ApplicationSettingImplementation
end end
def domain_whitelist_raw=(values) def domain_whitelist_raw=(values)
self.domain_whitelist = [] self.domain_whitelist = domain_string_to_array(values)
self.domain_whitelist = values.split(DOMAIN_LIST_SEPARATOR)
self.domain_whitelist.reject! { |d| d.empty? }
self.domain_whitelist
end end
def domain_blacklist_raw=(values) def domain_blacklist_raw=(values)
self.domain_blacklist = [] self.domain_blacklist = domain_string_to_array(values)
self.domain_blacklist = values.split(DOMAIN_LIST_SEPARATOR) end
self.domain_blacklist.reject! { |d| d.empty? }
self.domain_blacklist def asset_proxy_whitelist=(values)
values = domain_string_to_array(values) if values.is_a?(String)
# make sure we always whitelist the running host
values << Gitlab.config.gitlab.host unless values.include?(Gitlab.config.gitlab.host)
self[:asset_proxy_whitelist] = values
end end
def domain_blacklist_file=(file) def domain_blacklist_file=(file)
...@@ -276,4 +280,10 @@ module ApplicationSettingImplementation ...@@ -276,4 +280,10 @@ module ApplicationSettingImplementation
def expire_performance_bar_allowed_user_ids_cache def expire_performance_bar_allowed_user_ids_cache
Gitlab::PerformanceBar.expire_allowed_user_ids_cache Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end end
def domain_string_to_array(values)
domain_list = values.split(DOMAIN_LIST_SEPARATOR).map(&:strip)
domain_list.reject! { |d| d.empty? }
domain_list
end
end end
...@@ -6,6 +6,8 @@ module ApplicationSettings ...@@ -6,6 +6,8 @@ module ApplicationSettings
attr_reader :params, :application_setting attr_reader :params, :application_setting
MARKDOWN_CACHE_INVALIDATING_PARAMS = %w(asset_proxy_enabled asset_proxy_url asset_proxy_secret_key asset_proxy_whitelist).freeze
def execute def execute
validate_classification_label(application_setting, :external_authorization_service_default_label) validate_classification_label(application_setting, :external_authorization_service_default_label)
...@@ -23,7 +25,13 @@ module ApplicationSettings ...@@ -23,7 +25,13 @@ module ApplicationSettings
params[:usage_stats_set_by_user_id] = current_user.id params[:usage_stats_set_by_user_id] = current_user.id
end end
@application_setting.update(@params) @application_setting.assign_attributes(params)
if invalidate_markdown_cache?
@application_setting[:local_markdown_version] = @application_setting.local_markdown_version + 1
end
@application_setting.save
end end
private private
...@@ -32,6 +40,11 @@ module ApplicationSettings ...@@ -32,6 +40,11 @@ module ApplicationSettings
params.key?(:usage_ping_enabled) || params.key?(:version_check_enabled) params.key?(:usage_ping_enabled) || params.key?(:version_check_enabled)
end end
def invalidate_markdown_cache?
!params.key?(:local_markdown_version) &&
(@application_setting.changes.keys & MARKDOWN_CACHE_INVALIDATING_PARAMS).any?
end
def update_terms(terms) def update_terms(terms)
return unless terms.present? return unless terms.present?
... ...
......
---
title: Added image proxy to mitigate potential stealing of IP addresses
merge_request:
author:
type: security
#
# Asset proxy settings
#
ActiveSupport.on_load(:active_record) do
Banzai::Filter::AssetProxyFilter.initialize_settings
end
if Shard.connected? && !Gitlab::Database.read_only? # The `table_exists?` check is needed because during our migration rollback testing,
# `Shard.connected?` could be cached and return true even though the table doesn't exist
if Shard.connected? && Shard.table_exists? && !Gitlab::Database.read_only?
Shard.populate! Shard.populate!
end end
# frozen_string_literal: true
class AddAssetProxySettings < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :application_settings, :asset_proxy_enabled, :boolean, default: false, null: false
add_column :application_settings, :asset_proxy_url, :string
add_column :application_settings, :asset_proxy_whitelist, :text
add_column :application_settings, :encrypted_asset_proxy_secret_key, :text
add_column :application_settings, :encrypted_asset_proxy_secret_key_iv, :string
end
end
...@@ -228,6 +228,11 @@ ActiveRecord::Schema.define(version: 2019_08_16_151221) do ...@@ -228,6 +228,11 @@ ActiveRecord::Schema.define(version: 2019_08_16_151221) do
t.boolean "lock_memberships_to_ldap", default: false, null: false t.boolean "lock_memberships_to_ldap", default: false, null: false
t.boolean "time_tracking_limit_to_hours", default: false, null: false t.boolean "time_tracking_limit_to_hours", default: false, null: false
t.string "grafana_url", default: "/-/grafana", null: false t.string "grafana_url", default: "/-/grafana", null: false
t.boolean "asset_proxy_enabled", default: false, null: false
t.string "asset_proxy_url"
t.text "asset_proxy_whitelist"
t.text "encrypted_asset_proxy_secret_key"
t.string "encrypted_asset_proxy_secret_key_iv"
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
... ...
......
...@@ -39,6 +39,7 @@ Example response: ...@@ -39,6 +39,7 @@ Example response:
"session_expire_delay" : 10080, "session_expire_delay" : 10080,
"home_page_url" : null, "home_page_url" : null,
"default_snippet_visibility" : "private", "default_snippet_visibility" : "private",
"outbound_local_requests_whitelist": [],
"domain_whitelist" : [], "domain_whitelist" : [],
"domain_blacklist_enabled" : false, "domain_blacklist_enabled" : false,
"domain_blacklist" : [], "domain_blacklist" : [],
...@@ -63,7 +64,10 @@ Example response: ...@@ -63,7 +64,10 @@ Example response:
"performance_bar_allowed_group_id": 42, "performance_bar_allowed_group_id": 42,
"instance_statistics_visibility_private": false, "instance_statistics_visibility_private": false,
"user_show_add_ssh_key_message": true, "user_show_add_ssh_key_message": true,
"local_markdown_version": 0 "local_markdown_version": 0,
"asset_proxy_enabled": true,
"asset_proxy_url": "https://assets.example.com",
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"]
} }
``` ```
...@@ -113,6 +117,7 @@ Example response: ...@@ -113,6 +117,7 @@ Example response:
"default_project_visibility": "internal", "default_project_visibility": "internal",
"default_snippet_visibility": "private", "default_snippet_visibility": "private",
"default_group_visibility": "private", "default_group_visibility": "private",
"outbound_local_requests_whitelist": [],
"domain_whitelist": [], "domain_whitelist": [],
"domain_blacklist_enabled" : false, "domain_blacklist_enabled" : false,
"domain_blacklist" : [], "domain_blacklist" : [],
...@@ -136,6 +141,9 @@ Example response: ...@@ -136,6 +141,9 @@ Example response:
"user_show_add_ssh_key_message": true, "user_show_add_ssh_key_message": true,
"file_template_project_id": 1, "file_template_project_id": 1,
"local_markdown_version": 0, "local_markdown_version": 0,
"asset_proxy_enabled": true,
"asset_proxy_url": "https://assets.example.com",
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"],
"geo_node_allowed_ips": "0.0.0.0/0, ::/0" "geo_node_allowed_ips": "0.0.0.0/0, ::/0"
} }
``` ```
...@@ -176,6 +184,10 @@ are listed in the descriptions of the relevant settings. ...@@ -176,6 +184,10 @@ are listed in the descriptions of the relevant settings.
| `akismet_enabled` | boolean | no | (**If enabled, requires:** `akismet_api_key`) Enable or disable akismet spam protection. | | `akismet_enabled` | boolean | no | (**If enabled, requires:** `akismet_api_key`) Enable or disable akismet spam protection. |
| `allow_group_owners_to_manage_ldap` | boolean | no | **(PREMIUM)** Set to `true` to allow group owners to manage LDAP | | `allow_group_owners_to_manage_ldap` | boolean | no | **(PREMIUM)** Set to `true` to allow group owners to manage LDAP |
| `allow_local_requests_from_hooks_and_services` | boolean | no | Allow requests to the local network from hooks and services. | | `allow_local_requests_from_hooks_and_services` | boolean | no | Allow requests to the local network from hooks and services. |
| `asset_proxy_enabled` | boolean | no | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. GitLab restart is required to apply changes. |
| `asset_proxy_secret_key` | string | no | Shared secret with the asset proxy server. GitLab restart is required to apply changes. |
| `asset_proxy_url` | string | no | URL of the asset proxy server. GitLab restart is required to apply changes. |
| `asset_proxy_whitelist` | string or array of strings | no | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. GitLab restart is required to apply changes. |
| `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. | | `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. |
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. | | `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. | | `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. |
...@@ -193,6 +205,7 @@ are listed in the descriptions of the relevant settings. ...@@ -193,6 +205,7 @@ are listed in the descriptions of the relevant settings.
| `domain_blacklist` | array of strings | required by: `domain_blacklist_enabled` | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. | | `domain_blacklist` | array of strings | required by: `domain_blacklist_enabled` | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
| `domain_blacklist_enabled` | boolean | no | (**If enabled, requires:** `domain_blacklist`) Allows blocking sign-ups from emails from specific domains. | | `domain_blacklist_enabled` | boolean | no | (**If enabled, requires:** `domain_blacklist`) Allows blocking sign-ups from emails from specific domains. |
| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. | | `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. |
| `outbound_local_requests_whitelist` | array of strings | no | Define a list of trusted domains or ip addresses to which local requests are allowed when local requests for hooks and services are disabled.
| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. | | `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. | | `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. |
| `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. | | `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. |
...@@ -227,7 +240,7 @@ are listed in the descriptions of the relevant settings. ...@@ -227,7 +240,7 @@ are listed in the descriptions of the relevant settings.
| `gravatar_enabled` | boolean | no | Enable Gravatar. | | `gravatar_enabled` | boolean | no | Enable Gravatar. |
| `hashed_storage_enabled` | boolean | no | Create new projects using hashed storage paths: Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. (EXPERIMENTAL) | | `hashed_storage_enabled` | boolean | no | Create new projects using hashed storage paths: Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. (EXPERIMENTAL) |
| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. | | `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. |
| `help_page_support_url` | string | no | Alternate support URL for help page. | | `help_page_support_url` | string | no | Alternate support URL for help page and help dropdown. |
| `help_page_text` | string | no | Custom text displayed on the help page. | | `help_page_text` | string | no | Custom text displayed on the help page. |
| `help_text` | string | no | **(PREMIUM)** GitLab server administrator information | | `help_text` | string | no | **(PREMIUM)** GitLab server administrator information |
| `hide_third_party_offers` | boolean | no | Do not display offers from third parties within GitLab. | | `hide_third_party_offers` | boolean | no | Do not display offers from third parties within GitLab. |
... ...
......
...@@ -17,3 +17,4 @@ type: index ...@@ -17,3 +17,4 @@ type: index
- [Enforce Two-factor authentication](two_factor_authentication.md) - [Enforce Two-factor authentication](two_factor_authentication.md)
- [Send email confirmation on sign-up](user_email_confirmation.md) - [Send email confirmation on sign-up](user_email_confirmation.md)
- [Security of running jobs](https://docs.gitlab.com/runner/security/) - [Security of running jobs](https://docs.gitlab.com/runner/security/)
- [Proxying images](asset_proxy.md)
A possible security concern when managing a public facing GitLab instance is
the ability to steal a users IP address by referencing images in issues, comments, etc.
For example, adding `![Example image](http://example.com/example.png)` to
an issue description will cause the image to be loaded from the external
server in order to be displayed. However this also allows the external server
to log the IP address of the user.
One way to mitigate this is by proxying any external images to a server you
control. GitLab handles this by allowing you to run the "Camo" server
[cactus/go-camo](https://github.com/cactus/go-camo#how-it-works).
The image request is sent to the Camo server, which then makes the request for
the original image. This way an attacker only ever seems the IP address
of your Camo server.
Once you have your Camo server up and running, you can configure GitLab to
proxy image requests to it. The following settings are supported:
| Attribute | Description |
| --------- | ----------- |
| `asset_proxy_enabled` | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. |
| `asset_proxy_secret_key` | Shared secret with the asset proxy server. |
| `asset_proxy_url` | URL of the asset proxy server. |
| `asset_proxy_whitelist` | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. |
These can be set via the [Application setting API](../api/settings.md)
Note that a GitLab restart is required to apply any changes.
...@@ -1160,6 +1160,9 @@ module API ...@@ -1160,6 +1160,9 @@ module API
attributes.delete(:performance_bar_allowed_group_path) attributes.delete(:performance_bar_allowed_group_path)
attributes.delete(:performance_bar_enabled) attributes.delete(:performance_bar_enabled)
# let's not expose the secret key in a response
attributes.delete(:asset_proxy_secret_key)
attributes attributes
end end
... ...
......
...@@ -36,6 +36,10 @@ module API ...@@ -36,6 +36,10 @@ module API
given akismet_enabled: ->(val) { val } do given akismet_enabled: ->(val) { val } do
requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com' requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
end end
optional :asset_proxy_enabled, type: Boolean, desc: 'Enable proxying of assets'
optional :asset_proxy_url, type: String, desc: 'URL of the asset proxy server'
optional :asset_proxy_secret_key, type: String, desc: 'Shared secret with the asset proxy server'
optional :asset_proxy_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted.'
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group' optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group'
...@@ -59,7 +63,7 @@ module API ...@@ -59,7 +63,7 @@ module API
optional :grafana_url, type: String, desc: 'Grafana URL' optional :grafana_url, type: String, desc: 'Grafana URL'
optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled' optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled'
optional :help_page_hide_commercial_content, type: Boolean, desc: 'Hide marketing-related entries from help' optional :help_page_hide_commercial_content, type: Boolean, desc: 'Hide marketing-related entries from help'
optional :help_page_support_url, type: String, desc: 'Alternate support URL for help page' optional :help_page_support_url, type: String, desc: 'Alternate support URL for help page and help dropdown'
optional :help_page_text, type: String, desc: 'Custom text displayed on the help page' optional :help_page_text, type: String, desc: 'Custom text displayed on the help page'
optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page' optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page'
optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)' optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)'
...@@ -123,7 +127,7 @@ module API ...@@ -123,7 +127,7 @@ module API
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins' optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins'
optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated" optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated'
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction", optional :"#{type}_key_restriction",
... ...
......
# frozen_string_literal: true
module API
module Validations
module Types
class CommaSeparatedToArray
def self.coerce
lambda do |value|
case value
when String
value.split(',').map(&:strip)
when Array
value.map { |v| v.to_s.split(',').map(&:strip) }.flatten
else
[]
end
end
end
end
end
end
end
# frozen_string_literal: true
module Banzai
module Filter
# Proxy's images/assets to another server. Reduces mixed content warnings
# as well as hiding the customer's IP address when requesting images.
# Copies the original img `src` to `data-canonical-src` then replaces the
# `src` with a new url to the proxy server.
class AssetProxyFilter < HTML::Pipeline::CamoFilter
def initialize(text, context = nil, result = nil)
super
end
def validate
needs(:asset_proxy, :asset_proxy_secret_key) if asset_proxy_enabled?
end
def asset_host_whitelisted?(host)
context[:asset_proxy_domain_regexp] ? context[:asset_proxy_domain_regexp].match?(host) : false
end
def self.transform_context(context)
context[:disable_asset_proxy] = !Gitlab.config.asset_proxy.enabled
unless context[:disable_asset_proxy]
context[:asset_proxy_enabled] = !context[:disable_asset_proxy]
context[:asset_proxy] = Gitlab.config.asset_proxy.url
context[:asset_proxy_secret_key] = Gitlab.config.asset_proxy.secret_key
context[:asset_proxy_domain_regexp] = Gitlab.config.asset_proxy.domain_regexp
end
context
end
# called during an initializer. Caching the values in Gitlab.config significantly increased
# performance, rather than querying Gitlab::CurrentSettings.current_application_settings
# over and over. However, this does mean that the Rails servers need to get restarted
# whenever the application settings are changed
def self.initialize_settings
application_settings = Gitlab::CurrentSettings.current_application_settings
Gitlab.config['asset_proxy'] ||= Settingslogic.new({})
if application_settings.respond_to?(:asset_proxy_enabled)
Gitlab.config.asset_proxy['enabled'] = application_settings.asset_proxy_enabled
Gitlab.config.asset_proxy['url'] = application_settings.asset_proxy_url
Gitlab.config.asset_proxy['secret_key'] = application_settings.asset_proxy_secret_key
Gitlab.config.asset_proxy['whitelist'] = application_settings.asset_proxy_whitelist || [Gitlab.config.gitlab.host]
Gitlab.config.asset_proxy['domain_regexp'] = compile_whitelist(Gitlab.config.asset_proxy.whitelist)
else
Gitlab.config.asset_proxy['enabled'] = ::ApplicationSetting.defaults[:asset_proxy_enabled]
end
end
def self.compile_whitelist(domain_list)
return if domain_list.empty?
escaped = domain_list.map { |domain| Regexp.escape(domain).gsub('\*', '.*?') }
Regexp.new("^(#{escaped.join('|')})$", Regexp::IGNORECASE)
end
end
end
end
...@@ -14,10 +14,10 @@ module Banzai ...@@ -14,10 +14,10 @@ module Banzai
# such as on `mailto:` links. Since we've been using it, do an # such as on `mailto:` links. Since we've been using it, do an
# initial parse for validity and then use Addressable # initial parse for validity and then use Addressable
# for IDN support, etc # for IDN support, etc
uri = uri_strict(node['href'].to_s) uri = uri_strict(node_src(node))
if uri if uri
node.set_attribute('href', uri.to_s) node.set_attribute(node_src_attribute(node), uri.to_s)
addressable_uri = addressable_uri(node['href']) addressable_uri = addressable_uri(node_src(node))
else else
addressable_uri = nil addressable_uri = nil
end end
...@@ -35,6 +35,16 @@ module Banzai ...@@ -35,6 +35,16 @@ module Banzai
private private
# if this is a link to a proxied image, then `src` is already the correct
# proxied url, so work with the `data-canonical-src`
def node_src_attribute(node)
node['data-canonical-src'] ? 'data-canonical-src' : 'href'
end
def node_src(node)
node[node_src_attribute(node)]
end
def uri_strict(href) def uri_strict(href)
URI.parse(href) URI.parse(href)
rescue URI::Error rescue URI::Error
...@@ -72,7 +82,7 @@ module Banzai ...@@ -72,7 +82,7 @@ module Banzai
return unless uri return unless uri
return unless context[:emailable_links] return unless context[:emailable_links]
unencoded_uri_str = Addressable::URI.unencode(node['href']) unencoded_uri_str = Addressable::URI.unencode(node_src(node))
if unencoded_uri_str == node.content && idn?(uri) if unencoded_uri_str == node.content && idn?(uri)
node.content = uri.normalize node.content = uri.normalize
... ...
......
...@@ -18,6 +18,9 @@ module Banzai ...@@ -18,6 +18,9 @@ module Banzai
rel: 'noopener noreferrer' rel: 'noopener noreferrer'
) )
# make sure the original non-proxied src carries over to the link
link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src']
link.children = img.clone link.children = img.clone
img.replace(link) img.replace(link)
... ...
......
...@@ -23,6 +23,14 @@ module Banzai ...@@ -23,6 +23,14 @@ module Banzai
"'.#{ext}' = substring(@src, string-length(@src) - #{ext.size})" "'.#{ext}' = substring(@src, string-length(@src) - #{ext.size})"
end end
if context[:asset_proxy_enabled].present?
src_query.concat(
UploaderHelper::VIDEO_EXT.map do |ext|
"'.#{ext}' = substring(@data-canonical-src, string-length(@data-canonical-src) - #{ext.size})"
end
)
end
"descendant-or-self::img[not(ancestor::a) and (#{src_query.join(' or ')})]" "descendant-or-self::img[not(ancestor::a) and (#{src_query.join(' or ')})]"
end end
end end
...@@ -48,6 +56,13 @@ module Banzai ...@@ -48,6 +56,13 @@ module Banzai
target: '_blank', target: '_blank',
rel: 'noopener noreferrer', rel: 'noopener noreferrer',
title: "Download '#{element['title'] || element['alt']}'") title: "Download '#{element['title'] || element['alt']}'")
# make sure the original non-proxied src carries over
if element['data-canonical-src']
video['data-canonical-src'] = element['data-canonical-src']
link['data-canonical-src'] = element['data-canonical-src']
end
download_paragraph = doc.document.create_element('p') download_paragraph = doc.document.create_element('p')
download_paragraph.children = link download_paragraph.children = link
... ...
......
...@@ -6,12 +6,17 @@ module Banzai ...@@ -6,12 +6,17 @@ module Banzai
def self.filters def self.filters
FilterArray[ FilterArray[
Filter::AsciiDocSanitizationFilter, Filter::AsciiDocSanitizationFilter,
Filter::AssetProxyFilter,
Filter::SyntaxHighlightFilter, Filter::SyntaxHighlightFilter,
Filter::ExternalLinkFilter, Filter::ExternalLinkFilter,
Filter::PlantumlFilter, Filter::PlantumlFilter,
Filter::AsciiDocPostProcessingFilter Filter::AsciiDocPostProcessingFilter
] ]
end end
def self.transform_context(context)
Filter::AssetProxyFilter.transform_context(context)
end
end end
end end
end end