Commit a8c9d90f authored by Marius Bobin's avatar Marius Bobin

Merge branch 'feature/gb/remove-unsafe-regexps' into 'master'

Remove support for unsafe regular expressions

See merge request gitlab-org/gitlab!79611
parents 7204aec7 4c75b10e
--- ---
name: allow_unsafe_ruby_regexp name: disable_unsafe_regexp
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/10566 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79611
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/257849 rollout_issue_url:
milestone: '11.10' milestone: '14.9'
type: development type: development
group: group::pipeline execution group: group::pipeline execution
default_enabled: false default_enabled: false
...@@ -834,13 +834,9 @@ due to computational complexity, and some features, like negative lookaheads, be ...@@ -834,13 +834,9 @@ due to computational complexity, and some features, like negative lookaheads, be
Only a subset of features provided by [Ruby Regexp](https://ruby-doc.org/core/Regexp.html) Only a subset of features provided by [Ruby Regexp](https://ruby-doc.org/core/Regexp.html)
are now supported. are now supported.
From GitLab 11.9.7 to GitLab 12.0, GitLab provided a feature flag to From GitLab 11.9.7 to GitLab 14.9, GitLab provided a feature flag to let you
let you use unsafe regexp syntax. After migrating to safe syntax, you should disable use unsafe regexp syntax. We've fully migrated to RE2 now, and that feature
this feature flag again: flag is no longer available.
```ruby
Feature.enable(:allow_unsafe_ruby_regexp)
```
## CI/CD variable expressions ## CI/CD variable expressions
......
...@@ -65,11 +65,26 @@ RSpec.describe PushRule, :saas do ...@@ -65,11 +65,26 @@ RSpec.describe PushRule, :saas do
subject.branch_name_allowed?('ee-feature-ee') subject.branch_name_allowed?('ee-feature-ee')
end end
it 'falls back to ruby regex engine' do context 'when unsafe regexps are available' do
push_rule.update_column(:branch_name_regex, '(ee|ce).*\1') it 'falls back to ruby regex engine' do
stub_feature_flags(disable_unsafe_regexp: false)
expect(subject.branch_name_allowed?('ee-feature-ee')).to be true push_rule.update_column(:branch_name_regex, '(ee|ce).*\1')
expect(subject.branch_name_allowed?('ee-feature-ce')).to be false
expect(subject.branch_name_allowed?('ee-feature-ee')).to be true
expect(subject.branch_name_allowed?('ee-feature-ce')).to be false
end
end
context 'when unsafe regexps are disabled' do
it 'raises an exception' do
stub_feature_flags(disable_unsafe_regexp: true)
push_rule.update_column(:branch_name_regex, '(ee|ce).*\1')
expect { subject.branch_name_allowed?('ee-feature-ee') }
.to raise_error(described_class::MatchError)
end
end end
end end
end end
......
...@@ -36,7 +36,7 @@ module Gitlab ...@@ -36,7 +36,7 @@ module Gitlab
# the pattern matching does not work for merge requests pipelines # the pattern matching does not work for merge requests pipelines
if pipeline.branch? || pipeline.tag? if pipeline.branch? || pipeline.tag?
regexp = Gitlab::UntrustedRegexp::RubySyntax regexp = Gitlab::UntrustedRegexp::RubySyntax
.fabricate(pattern, fallback: true, project: pipeline.project) .fabricate(pattern, project: pipeline.project)
if regexp if regexp
regexp.match?(pipeline.ref) regexp.match?(pipeline.ref)
......
...@@ -17,7 +17,7 @@ module Gitlab ...@@ -17,7 +17,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, array_of_strings_or_regexps_with_fallback: true validates :config, array_of_strings_or_regexps: true
end end
def value def value
...@@ -38,7 +38,7 @@ module Gitlab ...@@ -38,7 +38,7 @@ module Gitlab
validate :variables_expressions_syntax validate :variables_expressions_syntax
with_options allow_nil: true do with_options allow_nil: true do
validates :refs, array_of_strings_or_regexps_with_fallback: true validates :refs, array_of_strings_or_regexps: true
validates :kubernetes, allowed_values: %w[active] validates :kubernetes, allowed_values: %w[active]
validates :variables, array_of_strings: true validates :variables, array_of_strings: true
validates :changes, array_of_strings: true validates :changes, array_of_strings: true
......
...@@ -228,12 +228,6 @@ module Gitlab ...@@ -228,12 +228,6 @@ module Gitlab
end end
end end
protected
def fallback
false
end
private private
def matches_syntax?(value) def matches_syntax?(value)
...@@ -242,7 +236,7 @@ module Gitlab ...@@ -242,7 +236,7 @@ module Gitlab
def validate_regexp(value) def validate_regexp(value)
matches_syntax?(value) && matches_syntax?(value) &&
Gitlab::UntrustedRegexp::RubySyntax.valid?(value, fallback: fallback) Gitlab::UntrustedRegexp::RubySyntax.valid?(value)
end end
end end
...@@ -271,27 +265,6 @@ module Gitlab ...@@ -271,27 +265,6 @@ module Gitlab
end end
end end
class ArrayOfStringsOrRegexpsWithFallbackValidator < ArrayOfStringsOrRegexpsValidator
protected
# TODO
#
# Remove ArrayOfStringsOrRegexpsWithFallbackValidator class too when
# you are removing the `:allow_unsafe_ruby_regexp` feature flag.
#
def validation_message
if ::Feature.enabled?(:allow_unsafe_ruby_regexp, default_enabled: :yaml)
'should be an array of strings or regular expressions'
else
super
end
end
def fallback
true
end
end
class ArrayOfStringsOrStringValidator < RegexpValidator class ArrayOfStringsOrStringValidator < RegexpValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
unless validate_array_of_strings_or_string(value) unless validate_array_of_strings_or_string(value)
......
...@@ -61,6 +61,16 @@ module Gitlab ...@@ -61,6 +61,16 @@ module Gitlab
def self.with_fallback(pattern, multiline: false) def self.with_fallback(pattern, multiline: false)
UntrustedRegexp.new(pattern, multiline: multiline) UntrustedRegexp.new(pattern, multiline: multiline)
rescue RegexpError rescue RegexpError
raise if Feature.enabled?(:disable_unsafe_regexp, default_enabled: :yaml)
if Feature.enabled?(:ci_unsafe_regexp_logger, type: :ops, default_enabled: :yaml)
Gitlab::AppJsonLogger.info(
class: self.name,
regexp: pattern.to_s,
fabricated: 'unsafe ruby regexp'
)
end
Regexp.new(pattern) Regexp.new(pattern)
end end
......
...@@ -16,40 +16,23 @@ module Gitlab ...@@ -16,40 +16,23 @@ module Gitlab
# The regexp can match the pattern `/.../`, but may not be fabricatable: # The regexp can match the pattern `/.../`, but may not be fabricatable:
# it can be invalid or incomplete: `/match ( string/` # it can be invalid or incomplete: `/match ( string/`
def self.valid?(pattern, fallback: false) def self.valid?(pattern)
!!self.fabricate(pattern, fallback: fallback) !!self.fabricate(pattern)
end end
def self.fabricate(pattern, fallback: false, project: nil) def self.fabricate(pattern, project: nil)
self.fabricate!(pattern, fallback: fallback, project: project) self.fabricate!(pattern, project: project)
rescue RegexpError rescue RegexpError
nil nil
end end
def self.fabricate!(pattern, fallback: false, project: nil) def self.fabricate!(pattern, project: nil)
raise RegexpError, 'Pattern is not string!' unless pattern.is_a?(String) raise RegexpError, 'Pattern is not string!' unless pattern.is_a?(String)
matches = pattern.match(PATTERN) matches = pattern.match(PATTERN)
raise RegexpError, 'Invalid regular expression!' if matches.nil? raise RegexpError, 'Invalid regular expression!' if matches.nil?
begin create_untrusted_regexp(matches[:regexp], matches[:flags])
create_untrusted_regexp(matches[:regexp], matches[:flags])
rescue RegexpError
raise unless fallback &&
Feature.enabled?(:allow_unsafe_ruby_regexp, default_enabled: :yaml)
if Feature.enabled?(:ci_unsafe_regexp_logger, type: :ops, default_enabled: :yaml)
Gitlab::AppJsonLogger.info(
class: self.name,
regexp: pattern.to_s,
fabricated: 'unsafe ruby regexp',
project_id: project&.id,
project_path: project&.full_path
)
end
create_ruby_regexp(matches[:regexp], matches[:flags])
end
end end
def self.create_untrusted_regexp(pattern, flags) def self.create_untrusted_regexp(pattern, flags)
...@@ -58,15 +41,6 @@ module Gitlab ...@@ -58,15 +41,6 @@ module Gitlab
UntrustedRegexp.new(pattern, multiline: false) UntrustedRegexp.new(pattern, multiline: false)
end end
private_class_method :create_untrusted_regexp private_class_method :create_untrusted_regexp
def self.create_ruby_regexp(pattern, flags)
options = 0
options += Regexp::IGNORECASE if flags&.include?('i')
options += Regexp::MULTILINE if flags&.include?('m')
Regexp.new(pattern, options)
end
private_class_method :create_ruby_regexp
end end
end end
end end
...@@ -149,26 +149,9 @@ RSpec.describe Gitlab::Ci::Build::Policy::Refs do ...@@ -149,26 +149,9 @@ RSpec.describe Gitlab::Ci::Build::Policy::Refs do
context 'when unsafe regexp is used' do context 'when unsafe regexp is used' do
let(:subject) { described_class.new(['/^(?!master).+/']) } let(:subject) { described_class.new(['/^(?!master).+/']) }
context 'when allow_unsafe_ruby_regexp is disabled' do it 'ignores invalid regexp' do
before do expect(subject)
stub_feature_flags(allow_unsafe_ruby_regexp: false) .not_to be_satisfied_by(pipeline)
end
it 'ignores invalid regexp' do
expect(subject)
.not_to be_satisfied_by(pipeline)
end
end
context 'when allow_unsafe_ruby_regexp is enabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
end
it 'is satisfied by regexp' do
expect(subject)
.to be_satisfied_by(pipeline)
end
end end
end end
end end
......
...@@ -59,9 +59,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do ...@@ -59,9 +59,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do
context 'when using an if: clause with lookahead regex character "?"' do context 'when using an if: clause with lookahead regex character "?"' do
let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } } let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } }
context 'when allow_unsafe_ruby_regexp is disabled' do it_behaves_like 'an invalid config', /invalid expression syntax/
it_behaves_like 'an invalid config', /invalid expression syntax/
end
end end
context 'when specifying unknown policy' do context 'when specifying unknown policy' do
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Policy do RSpec.describe Gitlab::Ci::Config::Entry::Policy do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
...@@ -45,29 +45,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Policy do ...@@ -45,29 +45,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Policy do
end end
context 'when using unsafe regexp' do context 'when using unsafe regexp' do
# When removed we could use `require 'fast_spec_helper'` again.
include StubFeatureFlags
let(:config) { ['/^(?!master).+/'] } let(:config) { ['/^(?!master).+/'] }
context 'when allow_unsafe_ruby_regexp is disabled' do it 'is not valid' do
before do expect(entry).not_to be_valid
stub_feature_flags(allow_unsafe_ruby_regexp: false)
end
it 'is not valid' do
expect(entry).not_to be_valid
end
end
context 'when allow_unsafe_ruby_regexp is enabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
end
it 'is valid' do
expect(entry).to be_valid
end
end end
end end
...@@ -106,29 +87,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Policy do ...@@ -106,29 +87,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Policy do
end end
context 'when using unsafe regexp' do context 'when using unsafe regexp' do
# When removed we could use `require 'fast_spec_helper'` again.
include StubFeatureFlags
let(:config) { { refs: ['/^(?!master).+/'] } } let(:config) { { refs: ['/^(?!master).+/'] } }
context 'when allow_unsafe_ruby_regexp is disabled' do it 'is not valid' do
before do expect(entry).not_to be_valid
stub_feature_flags(allow_unsafe_ruby_regexp: false)
end
it 'is not valid' do
expect(entry).not_to be_valid
end
end
context 'when allow_unsafe_ruby_regexp is enabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
end
it 'is valid' do
expect(entry).to be_valid
end
end end
end end
......
...@@ -92,12 +92,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do ...@@ -92,12 +92,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do
context 'when using an if: clause with lookahead regex character "?"' do context 'when using an if: clause with lookahead regex character "?"' do
let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } } let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } }
context 'when allow_unsafe_ruby_regexp is disabled' do it { is_expected.not_to be_valid }
it { is_expected.not_to be_valid }
it 'reports an error about invalid expression syntax' do it 'reports an error about invalid expression syntax' do
expect(subject.errors).to include(/invalid expression syntax/) expect(subject.errors).to include(/invalid expression syntax/)
end
end end
end end
......
...@@ -9,10 +9,6 @@ module Gitlab ...@@ -9,10 +9,6 @@ module Gitlab
subject { described_class.new(config, user: nil).execute } subject { described_class.new(config, user: nil).execute }
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
end
shared_examples 'returns errors' do |error_message| shared_examples 'returns errors' do |error_message|
it 'adds a message when an error is encountered' do it 'adds a message when an error is encountered' do
expect(subject.errors).to include(error_message) expect(subject.errors).to include(error_message)
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'fast_spec_helper'
RSpec.describe Gitlab::UntrustedRegexp::RubySyntax do RSpec.describe Gitlab::UntrustedRegexp::RubySyntax do
describe '.matches_syntax?' do describe '.matches_syntax?' do
...@@ -71,44 +71,6 @@ RSpec.describe Gitlab::UntrustedRegexp::RubySyntax do ...@@ -71,44 +71,6 @@ RSpec.describe Gitlab::UntrustedRegexp::RubySyntax do
end end
end end
context 'when unsafe regexp is used' do
include StubFeatureFlags
before do
# When removed we could use `require 'fast_spec_helper'` again.
stub_feature_flags(allow_unsafe_ruby_regexp: true)
allow(Gitlab::UntrustedRegexp).to receive(:new).and_raise(RegexpError)
end
context 'when no fallback is enabled' do
it 'raises an exception' do
expect { described_class.fabricate!('/something/') }
.to raise_error(RegexpError)
end
end
context 'when fallback is used' do
it 'fabricates regexp with a single flag' do
regexp = described_class.fabricate!('/something/i', fallback: true)
expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE)
end
it 'fabricates regexp with multiple flags' do
regexp = described_class.fabricate!('/something/im', fallback: true)
expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE | Regexp::MULTILINE)
end
it 'fabricates regexp without flags' do
regexp = described_class.fabricate!('/something/', fallback: true)
expect(regexp).to eq Regexp.new('something')
end
end
end
context 'when regexp is a raw pattern' do context 'when regexp is a raw pattern' do
it 'raises an error' do it 'raises an error' do
expect { described_class.fabricate!('some .* thing') } expect { described_class.fabricate!('some .* thing') }
......
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