raise-if-root 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,92 @@
1
+ # Raise If Root
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/raise-if-root.svg)](https://rubygems.org/gems/raise-if-root)
4
+ [![Build status](https://travis-ci.org/ab/raise-if-root.svg)](https://travis-ci.org/ab/raise-if-root)
5
+ [![Code Climate](https://codeclimate.com/github/ab/raise-if-root.svg)](https://codeclimate.com/github/ab/raise-if-root)
6
+ [![Inline Docs](http://inch-ci.org/github/ab/raise-if-root.svg?branch=master)](http://www.rubydoc.info/github/ab/raise-if-root/master)
7
+
8
+ *Raise If Root* is a small gem that helps prevent your application from ever
9
+ running as the root user (uid 0).
10
+
11
+ ## Why?
12
+
13
+ Many software systems rely on user privilege separation for security reasons.
14
+ Especially within containers or chroots, running as a non-privileged user gives
15
+ stronger isolation.
16
+
17
+ *Raise If Root* helps enforce that you never inadvertently load your
18
+ application code as root.
19
+
20
+ Will it protect you if your attacker is already running as root? Probably not.
21
+ But it does help remove opportunities for error, where you might accidentally
22
+ run root rake tasks, cron jobs, or deploy scripts.
23
+
24
+ ## Usage
25
+
26
+ Add the gem to your application's Gemfile:
27
+
28
+ ```ruby
29
+ gem 'raise-if-root', '~> 0'
30
+ ```
31
+
32
+ Require it from your main application code:
33
+
34
+ ```ruby
35
+ require 'raise-if-root'
36
+ ```
37
+
38
+ There is no step three! This will raise `RaiseIfRoot::AssertionFailed` if the
39
+ current uid is 0.
40
+
41
+ ### More complex patterns
42
+
43
+ See the [YARD documentation](http://www.rubydoc.info/github/ab/raise-if-root/master).
44
+
45
+ If you want to enforce that the application is running as a particular user,
46
+ there are several more specific functions available.
47
+
48
+ Load the library, which doesn't immediately raise when you load it.
49
+
50
+ ```ruby
51
+ # load the library, which doesn't raise
52
+ require 'raise-if-root/library'
53
+
54
+ # raise if running as uid 1000
55
+ RaiseIfRoot.raise_if_uid(1000)
56
+
57
+ # raise unless user is nobody
58
+ RaiseIfRoot.raise_if(username_not: 'nobody')
59
+
60
+ # raise with multiple conditions
61
+ RaiseIfRoot.raise_if(uid_not: 1000, gid_not: 500)
62
+ ```
63
+
64
+ ### Notification callbacks
65
+
66
+ If you want to sound the alarm with something more than just the exception, you
67
+ can add callbacks to send emails, smoke signals, etc.
68
+
69
+ ```ruby
70
+ # load the library, which doesn't raise
71
+ require 'raise-if-root/library'
72
+
73
+ RaiseIfRoot.add_assertion_callback do |err|
74
+ Mail.deliver do
75
+ from 'system@example.com'
76
+ to 'alerts@example.com'
77
+ subject 'App was run as root'
78
+ body "RaiseIfRoot is raising an exception:\n #{err.inspect}\n"
79
+ end
80
+ end
81
+
82
+ # ensure we're not root
83
+ RaiseIfRoot.raise_if_root
84
+ ```
85
+
86
+ ## Contributing
87
+
88
+ 1. Fork it
89
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
90
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
91
+ 4. Push to the branch (`git push origin my-new-feature`)
92
+ 5. Create new Pull Request
@@ -0,0 +1,15 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'bundler/setup'
3
+ require 'rspec/core/rake_task'
4
+
5
+ task :default do
6
+ sh 'rake -T'
7
+ end
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ def alias_task(alias_task, original)
12
+ desc "Alias for rake #{original}"
13
+ task alias_task, Rake.application[original].arg_names => original
14
+ end
15
+ alias_task(:test, :spec)
@@ -0,0 +1,3 @@
1
+ require_relative 'raise-if-root/library'
2
+
3
+ RaiseIfRoot.raise_if_root
@@ -0,0 +1,169 @@
1
+ require 'etc'
2
+
3
+ require_relative './version'
4
+
5
+ module RaiseIfRoot
6
+ # Error class for RaiseIfRoot assertion failures. Inherits directly from
7
+ # Exception because we don't want a bare rescue to catch this.
8
+ # rubocop:disable Lint/InheritException
9
+ class AssertionFailed < Exception; end
10
+
11
+ # Raise if the process UID/EUID is 0 or if the process GID/EGID is 0.
12
+ #
13
+ # @raise [AssertionFailed] if running as root
14
+ #
15
+ def self.raise_if_root
16
+ raise_if(uid: 0, gid: 0)
17
+ end
18
+
19
+ # Raise if the process UID or EUID equals +uid+.
20
+ #
21
+ # @param uid [Integer]
22
+ #
23
+ # @raise [AssertionFailed]
24
+ #
25
+ # @see .raise_if
26
+ #
27
+ def self.raise_if_uid(uid)
28
+ raise_if(uid: uid)
29
+ end
30
+
31
+ # Raise AssertionFailed if any of the specified conditions are met. This is
32
+ # the primary method powering RaiseIfRoot.
33
+ #
34
+ # @param uid [Integer] Raise if the process UID or EUID matches
35
+ # @param gid [Integer] Raise if the process GID or EGID matches
36
+ #
37
+ # @param uid_not [Integer] Raise if the process UID or EUID does not match
38
+ # the provided value
39
+ # @param gid_not [Integer] Raise if the process GID or EGID does not match
40
+ # the provided value
41
+ #
42
+ # @param username [String] Raise if the username of the process UID or EUID
43
+ # matches the provided value
44
+ # @param username_not [String] Raise if the username of the process UID or
45
+ # EUID does not match the provided value
46
+ #
47
+ # @raise [AssertionFailed] if any of the conditions match.
48
+ #
49
+ # rubocop:disable Metrics/ParameterLists
50
+ def self.raise_if(uid: nil, gid: nil, uid_not: nil, gid_not: nil,
51
+ username: nil, username_not: nil)
52
+ if uid
53
+ assert_not_equal('UID', Process.uid, uid)
54
+ assert_not_equal('EUID', Process.euid, uid)
55
+ end
56
+
57
+ if gid
58
+ assert_not_equal('GID', Process.gid, gid)
59
+ assert_not_equal('EGID', Process.egid, gid)
60
+ end
61
+
62
+ if uid_not
63
+ assert_equal('UID', Process.uid, uid_not)
64
+ assert_equal('EUID', Process.euid, uid_not)
65
+ end
66
+
67
+ if gid_not
68
+ assert_equal('GID', Process.gid, gid_not)
69
+ assert_equal('EGID', Process.egid, gid_not)
70
+ end
71
+
72
+ # raise if username
73
+ if username
74
+ assert_not_equal('username', Etc.getpwuid(Process.uid).name, username)
75
+ assert_not_equal('effective username', Etc.getpwuid(Process.euid).name,
76
+ username)
77
+ end
78
+
79
+ # raise unless username is username_not
80
+ if username_not
81
+ assert_equal('username', Etc.getpwuid(Process.uid).name, username_not)
82
+ assert_equal('effective username', Etc.getpwuid(Process.euid).name,
83
+ username_not)
84
+ end
85
+ end
86
+
87
+ # Assert that two values are equal. If they are not, run assertion callbacks
88
+ # and raise AssertionFailed.
89
+ #
90
+ # @param label [String] The label for the comparison we're making
91
+ # @param actual The actual value
92
+ # @param expected The expected value
93
+ #
94
+ # @raise [AssertionFailed] if the values are not equal
95
+ #
96
+ def self.assert_equal(label, actual, expected)
97
+ if expected.nil?
98
+ warn('warning: RaiseIfRoot.assert_equal called with expected=nil')
99
+ end
100
+ if actual != expected
101
+ err = new_assertion_failed(label, actual, expected)
102
+ run_assertion_callbacks(err)
103
+ raise err
104
+ end
105
+ end
106
+
107
+ # Assert that two values are not equal. But if they are equal, run assertion
108
+ # callbacks and raise AssertionFailed.
109
+ #
110
+ # @param label [String] The label for the comparison we're making
111
+ # @param actual The actual value
112
+ # @param expected The expected value
113
+ #
114
+ # @raise [AssertionFailed] if the values are equal
115
+ #
116
+ def self.assert_not_equal(label, actual, expected)
117
+ if actual == expected
118
+ err = new_assertion_failed(label, actual)
119
+ run_assertion_callbacks(err)
120
+ raise err
121
+ end
122
+ end
123
+
124
+ # Create a new AssertionFailed object.
125
+ #
126
+ # @param label [String] The label for the comparison we're making
127
+ # @param actual The actual value
128
+ # @param expected The expected value, if any
129
+ #
130
+ # @return [AssertionFailed]
131
+ #
132
+ def self.new_assertion_failed(label, actual, expected=nil)
133
+ # rubocop:disable Style/SpecialGlobalVars
134
+ message = "Process[#{$$}] #{label} is #{actual.inspect}"
135
+ if expected
136
+ message << ", expected #{expected.inspect}"
137
+ end
138
+
139
+ AssertionFailed.new(message)
140
+ end
141
+
142
+ # Add a callback to the list of assertion callbacks that are executed when an
143
+ # assertion fails. The callback will be passed one argument: the
144
+ # AssertionFailed exception object just before it is raised.
145
+ def self.add_assertion_callback(&block)
146
+ raise ArgumentError.new("Must pass block") unless block
147
+
148
+ assertion_callbacks << block
149
+ end
150
+
151
+ # The list of stored assertion callbacks. These are executed when an
152
+ # assertion fails just before the assertion is raised.
153
+ #
154
+ # @return [Array<Proc>]
155
+ #
156
+ def self.assertion_callbacks
157
+ @assertion_callbacks ||= []
158
+ end
159
+
160
+ # Execute all of the stored assertion callbacks.
161
+ #
162
+ # @param [AssertionFailed] err The exception object to pass to each callback.
163
+ #
164
+ # @return [Array] The collected return values of the callbacks.
165
+ #
166
+ def self.run_assertion_callbacks(err)
167
+ assertion_callbacks.map { |block| block.call(err) }
168
+ end
169
+ end
@@ -0,0 +1,4 @@
1
+ module RaiseIfRoot
2
+ # version string
3
+ VERSION = '0.0.1'.freeze
4
+ end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'raise-if-root/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'raise-if-root'
9
+ spec.version = RaiseIfRoot::VERSION
10
+ spec.authors = ['Andy Brody']
11
+ spec.email = ['git@abrody.com']
12
+ spec.summary = 'Library that raises when run as root user'
13
+ spec.description = <<-EOM
14
+ Raise If Root is a small library that raises an exception when run as the
15
+ root user (uid 0). This helps ensure that you never accidentally run your
16
+ application as root.
17
+ EOM
18
+ spec.homepage = 'https://github.com/ab/raise-if-root'
19
+ spec.license = 'GPL-3'
20
+
21
+ spec.files = `git ls-files`.split($/)
22
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.3'
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'rake'
29
+ spec.add_development_dependency 'rspec', '~> 3.0'
30
+ spec.add_development_dependency 'rubocop', '~> 0'
31
+ spec.add_development_dependency 'yard'
32
+
33
+ spec.required_ruby_version = '>= 2.0'
34
+ end
@@ -0,0 +1,88 @@
1
+ require_relative '../lib/raise-if-root/library'
2
+
3
+ # Given that it is always loaded, you are encouraged to keep this file as
4
+ # light-weight as possible. Requiring heavyweight dependencies from this file
5
+ # will add to the boot time of your test suite on EVERY test run, even for an
6
+ # individual file that may not need all of that loaded. Instead, consider making
7
+ # a separate helper file that requires the additional dependencies and performs
8
+ # the additional setup, and require it from the spec files that actually need
9
+ # it.
10
+ #
11
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
12
+ RSpec.configure do |config|
13
+ # rspec-expectations config goes here. You can use an alternate
14
+ # assertion/expectation library such as wrong or the stdlib/minitest
15
+ # assertions if you prefer.
16
+ config.expect_with :rspec do |expectations|
17
+ # This option will default to `true` in RSpec 4. It makes the `description`
18
+ # and `failure_message` of custom matchers include text for helper methods
19
+ # defined using `chain`, e.g.:
20
+ # be_bigger_than(2).and_smaller_than(4).description
21
+ # # => "be bigger than 2 and smaller than 4"
22
+ # ...rather than:
23
+ # # => "be bigger than 2"
24
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
25
+ end
26
+
27
+ # rspec-mocks config goes here. You can use an alternate test double
28
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
29
+ config.mock_with :rspec do |mocks|
30
+ # Prevents you from mocking or stubbing a method that does not exist on
31
+ # a real object. This is generally recommended, and will default to
32
+ # `true` in RSpec 4.
33
+ mocks.verify_partial_doubles = true
34
+ end
35
+
36
+ # The settings below are suggested to provide a good initial experience
37
+ # with RSpec, but feel free to customize to your heart's content.
38
+
39
+ # These two settings work together to allow you to limit a spec run
40
+ # to individual examples or groups you care about by tagging them with
41
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
42
+ # get run.
43
+ # config.filter_run :focus
44
+ # config.run_all_when_everything_filtered = true
45
+
46
+ # Allows RSpec to persist some state between runs in order to support
47
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
48
+ # you configure your source control system to ignore this file.
49
+ # config.example_status_persistence_file_path = "spec/examples.txt"
50
+
51
+ # Limits the available syntax to the non-monkey patched syntax that is
52
+ # recommended. For more details, see:
53
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
54
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
55
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
56
+ config.disable_monkey_patching!
57
+
58
+ # This setting enables warnings. It's recommended, but in some cases may
59
+ # be too noisy due to issues in dependencies.
60
+ config.warnings = true
61
+
62
+ # Many RSpec users commonly either run the entire suite or an individual
63
+ # file, and it's useful to allow more verbose output when running an
64
+ # individual spec file.
65
+ if config.files_to_run.one?
66
+ # Use the documentation formatter for detailed output,
67
+ # unless a formatter has already been configured
68
+ # (e.g. via a command-line flag).
69
+ config.default_formatter = 'doc'
70
+ end
71
+
72
+ # Print the 10 slowest examples and example groups at the
73
+ # end of the spec run, to help surface which specs are running
74
+ # particularly slow.
75
+ config.profile_examples = 10
76
+
77
+ # Run specs in random order to surface order dependencies. If you find an
78
+ # order dependency and want to debug it, you can fix the order by providing
79
+ # the seed, which is printed after each run.
80
+ # --seed 1234
81
+ config.order = :random
82
+
83
+ # Seed global randomization in this process using the `--seed` CLI option.
84
+ # Setting this allows you to use `--seed` to deterministically reproduce
85
+ # test failures related to randomization by passing the same `--seed` value
86
+ # as the one that triggered the failure.
87
+ Kernel.srand config.seed
88
+ end
@@ -0,0 +1,140 @@
1
+ # NB: These tests cannot be run as root (uid 0 or gid 0), sorry.
2
+ RSpec.describe RaiseIfRoot do
3
+
4
+ describe '.raise_if_root' do
5
+ it 'should raise if uid is 0' do
6
+ allow(Process).to receive(:uid).and_return(0)
7
+ expect {
8
+ RaiseIfRoot.raise_if_root
9
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bUID\b/)
10
+ end
11
+ it 'should raise if euid is 0' do
12
+ allow(Process).to receive(:euid).and_return(0)
13
+ expect {
14
+ RaiseIfRoot.raise_if_root
15
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bEUID\b/)
16
+ end
17
+ it 'should raise if gid is 0' do
18
+ allow(Process).to receive(:gid).and_return(0)
19
+ expect {
20
+ RaiseIfRoot.raise_if_root
21
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bGID\b/)
22
+ end
23
+ it 'should raise if egid is 0' do
24
+ allow(Process).to receive(:egid).and_return(0)
25
+ expect {
26
+ RaiseIfRoot.raise_if_root
27
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bEGID\b/)
28
+ end
29
+
30
+ it 'should not raise otherwise' do
31
+ expect(RaiseIfRoot.raise_if_root).to eq nil
32
+ end
33
+
34
+ it 'runs assertion callbacks' do
35
+ allow(Process).to receive(:uid).and_return(0)
36
+ expect(RaiseIfRoot).to receive(:run_assertion_callbacks).with(
37
+ instance_of(RaiseIfRoot::AssertionFailed)
38
+ ).once.and_call_original
39
+
40
+ expect {
41
+ RaiseIfRoot.raise_if_root
42
+ }.to raise_error(RaiseIfRoot::AssertionFailed)
43
+ end
44
+ end
45
+
46
+ describe '.add_assertion_callback' do
47
+ before do
48
+ RaiseIfRoot.assertion_callbacks.clear
49
+ end
50
+ after do
51
+ RaiseIfRoot.assertion_callbacks.clear
52
+ end
53
+
54
+ it 'adds a callback' do
55
+ expect(RaiseIfRoot.assertion_callbacks.length).to eq 0
56
+
57
+ RaiseIfRoot.add_assertion_callback { :returnvalue }
58
+ expect(RaiseIfRoot.assertion_callbacks.last).is_a?(Proc)
59
+ expect(RaiseIfRoot.assertion_callbacks.length).to eq 1
60
+
61
+ expect(RaiseIfRoot.run_assertion_callbacks(nil)).to eq([:returnvalue])
62
+ end
63
+ end
64
+
65
+ describe '.run_assertion_callbacks' do
66
+ before do
67
+ RaiseIfRoot.assertion_callbacks.clear
68
+ end
69
+ after do
70
+ RaiseIfRoot.assertion_callbacks.clear
71
+ end
72
+
73
+ it 'runs callbacks' do
74
+ sentinel = double('Sentinel')
75
+ expect(sentinel).to(receive(:callback).with(
76
+ instance_of(RaiseIfRoot::AssertionFailed)
77
+ ).twice.and_return(:callbackresponse1, :callbackresponse2))
78
+
79
+ expect(RaiseIfRoot.assertion_callbacks.length).to eq 0
80
+
81
+ RaiseIfRoot.add_assertion_callback { |err| sentinel.callback(err) }
82
+ RaiseIfRoot.add_assertion_callback { |err| sentinel.callback(err) }
83
+
84
+ expect(RaiseIfRoot.assertion_callbacks.length).to eq 2
85
+
86
+ expect(RaiseIfRoot.run_assertion_callbacks(
87
+ RaiseIfRoot::AssertionFailed.new
88
+ )).to eq([:callbackresponse1, :callbackresponse2])
89
+ end
90
+ end
91
+
92
+ describe '.raise_if' do
93
+ before do
94
+ allow(Process).to receive(:uid).and_return(5000)
95
+ allow(Process).to receive(:euid).and_return(5000)
96
+
97
+ allow(Process).to receive(:gid).and_return(5000)
98
+ allow(Process).to receive(:egid).and_return(5000)
99
+
100
+ @someuser = double('Etc::Passwd', name: 'someuser')
101
+ end
102
+
103
+ it 'does not raise by default' do
104
+ expect(RaiseIfRoot.raise_if).to eq nil
105
+ expect(RaiseIfRoot.raise_if(uid: 0)).to eq nil
106
+ expect(RaiseIfRoot.raise_if(gid: 0)).to eq nil
107
+ expect(RaiseIfRoot.raise_if(uid_not: 5000)).to eq nil
108
+ expect(RaiseIfRoot.raise_if(gid_not: 5000)).to eq nil
109
+ expect(RaiseIfRoot.raise_if(uid: 0, gid: 0)).to eq nil
110
+ end
111
+
112
+ it 'raises when any condition matches' do
113
+ expect {
114
+ RaiseIfRoot.raise_if(uid: 0, gid: 0, uid_not: 123, gid_not: 5000)
115
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bUID\b/)
116
+ expect {
117
+ RaiseIfRoot.raise_if(uid: 0, gid: 5000, uid_not: 5000, gid_not: 5000)
118
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bGID\b/)
119
+ expect {
120
+ RaiseIfRoot.raise_if(uid: 5000, gid: 0)
121
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bUID\b/)
122
+ end
123
+
124
+ it 'handles username and not_username' do
125
+ allow(Etc).to receive(:getpwuid).with(5000).and_return(@someuser)
126
+
127
+ expect {
128
+ RaiseIfRoot.raise_if(username: 'someuser')
129
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\busername\b/)
130
+
131
+ expect(RaiseIfRoot.raise_if(username: 'other')).to eq nil
132
+
133
+ expect {
134
+ RaiseIfRoot.raise_if(username_not: 'other')
135
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\busername\b/)
136
+
137
+ expect(RaiseIfRoot.raise_if(username_not: 'someuser')).to eq nil
138
+ end
139
+ end
140
+ end