Commit 1b181612 authored by Markus Koller's avatar Markus Koller Committed by Imre Farkas

Add limit for wiki page content size

This adds a new validation to restrict the maximum size of a wiki page,
which is only enforced when the content is updated, and only when using
the GitLab UI or API (not for Git pushes yet).

The default limit is 50MB, and can be changed through the Rails console
or the API.
parent 8404129c
...@@ -327,7 +327,8 @@ module ApplicationSettingsHelper ...@@ -327,7 +327,8 @@ module ApplicationSettingsHelper
:project_download_export_limit, :project_download_export_limit,
:group_import_limit, :group_import_limit,
:group_export_limit, :group_export_limit,
:group_download_export_limit :group_download_export_limit,
:wiki_page_max_content_bytes
] ]
end end
......
...@@ -272,6 +272,7 @@ class ApplicationSetting < ApplicationRecord ...@@ -272,6 +272,7 @@ class ApplicationSetting < ApplicationRecord
numericality: { greater_than_or_equal_to: 0 } numericality: { greater_than_or_equal_to: 0 }
validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 } validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 }
validates :wiki_page_max_content_bytes, numericality: { only_integer: true, greater_than: 0 }
validates :email_restrictions, untrusted_regexp: true validates :email_restrictions, untrusted_regexp: true
......
...@@ -164,7 +164,8 @@ module ApplicationSettingImplementation ...@@ -164,7 +164,8 @@ module ApplicationSettingImplementation
project_download_export_limit: 1, project_download_export_limit: 1,
group_import_limit: 6, group_import_limit: 6,
group_export_limit: 6, group_export_limit: 6,
group_download_export_limit: 1 group_download_export_limit: 1,
wiki_page_max_content_bytes: 50.megabytes
} }
end end
......
...@@ -65,6 +65,7 @@ class WikiPage ...@@ -65,6 +65,7 @@ class WikiPage
validates :title, presence: true validates :title, presence: true
validates :content, presence: true validates :content, presence: true
validate :validate_path_limits, if: :title_changed? validate :validate_path_limits, if: :title_changed?
validate :validate_content_size_limit, if: :content_changed?
# The GitLab Wiki instance. # The GitLab Wiki instance.
attr_reader :wiki attr_reader :wiki
...@@ -282,6 +283,10 @@ class WikiPage ...@@ -282,6 +283,10 @@ class WikiPage
end end
end end
def content_changed?
attributes[:content] != page&.text_data
end
# Updates the current @attributes hash by merging a hash of params # Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs) def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present? attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
...@@ -391,4 +396,15 @@ class WikiPage ...@@ -391,4 +396,15 @@ class WikiPage
}) })
end end
end end
def validate_content_size_limit
current_value = raw_content.to_s.bytesize
max_size = Gitlab::CurrentSettings.wiki_page_max_content_bytes
return if current_value <= max_size
errors.add(:content, _('is too long (%{current_value}). The maximum size is %{max_size}.') % {
current_value: ActiveSupport::NumberHelper.number_to_human_size(current_value),
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size)
})
end
end end
---
title: Add limit for wiki page content size
merge_request: 36729
author:
type: performance
# frozen_string_literal: true
class AddWikiPageMaxContentBytesToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :application_settings, :wiki_page_max_content_bytes, :bigint, default: 50.megabytes, null: false
end
end
...@@ -9172,6 +9172,7 @@ CREATE TABLE public.application_settings ( ...@@ -9172,6 +9172,7 @@ CREATE TABLE public.application_settings (
group_download_export_limit integer DEFAULT 1 NOT NULL, group_download_export_limit integer DEFAULT 1 NOT NULL,
maintenance_mode boolean DEFAULT false NOT NULL, maintenance_mode boolean DEFAULT false NOT NULL,
maintenance_mode_message text, maintenance_mode_message text,
wiki_page_max_content_bytes bigint DEFAULT 52428800 NOT NULL,
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)), CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)), CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
...@@ -23860,6 +23861,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -23860,6 +23861,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200712084655 20200712084655
20200712235622 20200712235622
20200713071042 20200713071042
20200713141854
20200713152443 20200713152443
20200716044023 20200716044023
20200716120419 20200716120419
......
...@@ -163,7 +163,11 @@ Learn how to install, configure, update, and maintain your GitLab instance. ...@@ -163,7 +163,11 @@ Learn how to install, configure, update, and maintain your GitLab instance.
## Snippet settings ## Snippet settings
- [Setting snippet content size limit](snippets/index.md): Set a maximum size limit for snippets' content. - [Setting snippet content size limit](snippets/index.md): Set a maximum content size limit for snippets.
## Wiki settings
- [Setting wiki page content size limit](wikis/index.md): Set a maximum content size limit for wiki pages.
## Git configuration options ## Git configuration options
......
...@@ -315,6 +315,7 @@ Set the limit to `0` to disable it. ...@@ -315,6 +315,7 @@ Set the limit to `0` to disable it.
## Wiki limits ## Wiki limits
- [Wiki page content size limit](wikis/index.md#wiki-page-content-size-limit).
- [Length restrictions for file and directory names](../user/project/wiki/index.md#length-restrictions-for-file-and-directory-names). - [Length restrictions for file and directory names](../user/project/wiki/index.md#length-restrictions-for-file-and-directory-names).
## Snippets limits ## Snippets limits
......
--- ---
type: reference, howto type: reference, howto
stage: Create
group: Editor
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
--- ---
# Snippets settings **(CORE ONLY)** # Snippets settings **(CORE ONLY)**
...@@ -10,15 +13,15 @@ Adjust the snippets' settings of your GitLab instance. ...@@ -10,15 +13,15 @@ Adjust the snippets' settings of your GitLab instance.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31133) in GitLab 12.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31133) in GitLab 12.6.
You can set a content size max limit in snippets. This limit can prevent You can set a maximum content size limit for snippets. This limit can prevent
abuses of the feature. The default content size limit is **52428800 Bytes** (50MB). abuse of the feature. The default value is **52428800 Bytes** (50 MB).
### How does it work? ### How does it work?
The content size limit will be applied when a snippet is created or The content size limit will be applied when a snippet is created or updated.
updated. Nevertheless, in order not to break any existing snippet,
the limit will only be enforced in stored snippets when the content In order not to break any existing snippets, the limit doesn't have any
is updated. effect on them until a snippet is edited again and the content changes.
### Snippets size limit configuration ### Snippets size limit configuration
...@@ -27,7 +30,7 @@ In order to configure this setting, use either the Rails console ...@@ -27,7 +30,7 @@ In order to configure this setting, use either the Rails console
or the [Application settings API](../../api/settings.md). or the [Application settings API](../../api/settings.md).
NOTE: **IMPORTANT:** NOTE: **IMPORTANT:**
The value of the limit **must** be in Bytes. The value of the limit **must** be in bytes.
#### Through the Rails console #### Through the Rails console
......
---
type: reference, howto
stage: Create
group: Knowledge
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Wiki settings **(CORE ONLY)**
Adjust the wiki settings of your GitLab instance.
## Wiki page content size limit
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31176) in GitLab 13.2.
You can set a maximum content size limit for wiki pages. This limit can prevent
abuse of the feature. The default value is **52428800 Bytes** (50 MB).
### How does it work?
The content size limit will be applied when a wiki page is created or updated
through the GitLab UI or API. Local changes pushed via Git will not be validated.
In order not to break any existing wiki pages, the limit doesn't have any
effect on them until a wiki page is edited again and the content changes.
### Wiki page content size limit configuration
This setting is not available through the [Admin Area settings](../../user/admin_area/settings/index.md).
In order to configure this setting, use either the Rails console
or the [Application settings API](../../api/settings.md).
NOTE: **IMPORTANT:**
The value of the limit **must** be in bytes.
#### Through the Rails console
The steps to configure this setting through the Rails console are:
1. Start the Rails console:
```shell
# For Omnibus installations
sudo gitlab-rails console
# For installations from source
sudo -u git -H bundle exec rails console -e production
```
1. Update the wiki page maximum content size:
```ruby
ApplicationSetting.first.update!(wiki_page_max_content_bytes: 50.megabytes)
```
To retrieve the current value, start the Rails console and run:
```ruby
Gitlab::CurrentSettings.wiki_page_max_content_bytes
```
#### Through the API
The process to set the wiki page size limit through the Application Settings API is
exactly the same as you would do to [update any other setting](../../api/settings.md#change-application-settings).
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/settings?wiki_page_max_content_bytes=52428800
```
You can also use the API to [retrieve the current value](../../api/settings.md#get-current-application-settings).
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/settings
```
...@@ -71,8 +71,10 @@ Example response: ...@@ -71,8 +71,10 @@ Example response:
"asset_proxy_url": "https://assets.example.com", "asset_proxy_url": "https://assets.example.com",
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"], "asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"],
"npm_package_requests_forwarding": true, "npm_package_requests_forwarding": true,
"snippet_size_limit": 52428800,
"issues_create_limit": 300, "issues_create_limit": 300,
"raw_blob_request_limit": 300 "raw_blob_request_limit": 300,
"wiki_page_max_content_bytes": 52428800
} }
``` ```
...@@ -161,8 +163,10 @@ Example response: ...@@ -161,8 +163,10 @@ Example response:
"allow_local_requests_from_web_hooks_and_services": true, "allow_local_requests_from_web_hooks_and_services": true,
"allow_local_requests_from_system_hooks": false, "allow_local_requests_from_system_hooks": false,
"npm_package_requests_forwarding": true, "npm_package_requests_forwarding": true,
"snippet_size_limit": 52428800,
"issues_create_limit": 300, "issues_create_limit": 300,
"raw_blob_request_limit": 300 "raw_blob_request_limit": 300,
"wiki_page_max_content_bytes": 52428800
} }
``` ```
...@@ -369,3 +373,4 @@ are listed in the descriptions of the relevant settings. ...@@ -369,3 +373,4 @@ are listed in the descriptions of the relevant settings.
| `snippet_size_limit` | integer | no | Max snippet content size in **bytes**. Default: 52428800 Bytes (50MB).| | `snippet_size_limit` | integer | no | Max snippet content size in **bytes**. Default: 52428800 Bytes (50MB).|
| `issues_create_limit` | integer | no | Max number of issue creation requests per minute per user. Default: 300. To disable throttling set to 0.| | `issues_create_limit` | integer | no | Max number of issue creation requests per minute per user. Default: 300. To disable throttling set to 0.|
| `raw_blob_request_limit` | integer | no | Max number of requests per minute for each raw path. Default: 300. To disable throttling set to 0.| | `raw_blob_request_limit` | integer | no | Max number of requests per minute for each raw path. Default: 300. To disable throttling set to 0.|
| `wiki_page_max_content_bytes` | integer | no | Max wiki page content size in **bytes**. Default: 52428800 Bytes (50MB).|
...@@ -72,6 +72,7 @@ RSpec.describe ApplicationSetting do ...@@ -72,6 +72,7 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) } it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) } it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) }
it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than(0) }
it { is_expected.to validate_presence_of(:max_artifacts_size) } it { is_expected.to validate_presence_of(:max_artifacts_size) }
it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) } it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) }
it { is_expected.to validate_presence_of(:max_pages_size) } it { is_expected.to validate_presence_of(:max_pages_size) }
......
...@@ -267,6 +267,53 @@ RSpec.describe WikiPage do ...@@ -267,6 +267,53 @@ RSpec.describe WikiPage do
expect(subject.errors.keys).to contain_exactly(:content) expect(subject.errors.keys).to contain_exactly(:content)
end end
describe 'content size validation' do
let(:limit) { 10 }
before do
stub_application_setting(wiki_page_max_content_bytes: limit)
end
it 'accepts content below the limit' do
subject.attributes[:content] = 'a' * 10
expect(subject).to be_valid
end
it 'rejects content exceeding the limit' do
subject.attributes[:content] = 'a' * 11
expect(subject).not_to be_valid
expect(subject.errors.messages).to eq(
content: ['is too long (11 Bytes). The maximum size is 10 Bytes.']
)
end
it 'counts content size in bytes rather than characters' do
subject.attributes[:content] = '💩💩💩'
expect(subject).not_to be_valid
expect(subject.errors.messages).to eq(
content: ['is too long (12 Bytes). The maximum size is 10 Bytes.']
)
end
context 'with an existing page exceeding the limit' do
let(:limit) { existing_page.content.bytesize - 1 }
it 'accepts content when it has not changed' do
expect(existing_page).to be_valid
end
it 'rejects content when it has changed' do
existing_page.attributes[:content] = 'a' * (limit + 1)
expect(existing_page).not_to be_valid
expect(existing_page.errors.keys).to contain_exactly(:content)
end
end
end
describe '#validate_path_limits' do describe '#validate_path_limits' do
let(:max_title) { Gitlab::WikiPages::MAX_TITLE_BYTES } let(:max_title) { Gitlab::WikiPages::MAX_TITLE_BYTES }
let(:max_directory) { Gitlab::WikiPages::MAX_DIRECTORY_BYTES } let(:max_directory) { Gitlab::WikiPages::MAX_DIRECTORY_BYTES }
...@@ -702,6 +749,50 @@ RSpec.describe WikiPage do ...@@ -702,6 +749,50 @@ RSpec.describe WikiPage do
end end
end end
describe '#content_changed?' do
context 'with a new page' do
subject { new_page }
it 'returns false if content is nil' do
subject.attributes[:content] = nil
expect(subject.content_changed?).to be(false)
end
it 'returns true if content is set' do
subject.attributes[:content] = 'new'
expect(subject.content_changed?).to be(true)
end
it 'returns true if content is blank' do
subject.attributes[:content] = ''
expect(subject.content_changed?).to be(true)
end
end
context 'with an existing page' do
subject { existing_page }
it 'returns false' do
expect(subject.content_changed?).to be(false)
end
it 'returns false if content is set to the same value' do
subject.attributes[:content] = 'test content'
expect(subject.content_changed?).to be(false)
end
it 'returns true if content is changed' do
subject.attributes[:content] = 'new'
expect(subject.content_changed?).to be(true)
end
end
end
describe '#path' do describe '#path' do
it 'returns the path when persisted' do it 'returns the path when persisted' do
expect(existing_page.path).to eq('test-page.md') expect(existing_page.path).to eq('test-page.md')
......
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