pledge 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7369f71bb2bfdbbea7dd07d98c86f443b8c0a09ddaf13090c6c0a379c2fe6eed
4
- data.tar.gz: bea75bb0d79544311c633aeb04ad7bcb3d2061ace0c6f9f0829e05dd387068e0
3
+ metadata.gz: 949027d706330a2286744e8230a2bf32dcc70eb96bc92d96ea3ecfcc4cc9c79f
4
+ data.tar.gz: 746d5466733720404a1ed65c53d635f67f90d7c187e8e77b30148bec9fe71e58
5
5
  SHA512:
6
- metadata.gz: 4031731b34fe1c2497cfa84ffad17b698ada329362cbb0f8730ea2bfa4f094c0429150b92364a8764d04eef1397be3cdb7d70145ccc9ea88b6e0dadb425d36b7
7
- data.tar.gz: 3509a67789e3876149e8880bc031fdb0d5df2bf111cd65b7f112132b1dd64696ff38ef9f70efc854d5ba99eb4c83d1256b4d68ba5805e879725fc5e6ed14a735
6
+ metadata.gz: 75456b41d7c3c77633ce3395cf9c55f5a3d3812ba99cbdeb7ef3a4ff3ef4c9fac4f7e83ecf7ab8501e54b07189b3a9aa53d16c589932f14c40b52e5fa0a3a733
7
+ data.tar.gz: a75f472530864f085cfd0aa427abd18a871708b1916b2d0f1fabed31a5164a73a695769884e10ab045ef6f1d28333a6bb75ed6081b092f0de92e9b7a1fba8af5
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ === 1.2.0 (2019-07-07)
2
+
3
+ * Add unveil library and Pledge.unveil for access to unveil(2) to control file system access (jeremyevans)
4
+
1
5
  === 1.1.0 (2019-04-25)
2
6
 
3
7
  * Support execpromises as optional second argument to Pledge.pledge (jeremyevans)
@@ -1,17 +1,17 @@
1
1
  = pledge
2
2
 
3
- pledge exposes OpenBSD's pledge(2) system call to ruby, allowing a
4
- program to restrict the types of operations the program
5
- can do after that point. Unlike other similar systems,
6
- pledge is specifically designed for programs that need to
7
- use a wide variety of operations on initialization, but
3
+ pledge exposes OpenBSD's pledge(2) and unveil(2) system
4
+ calls to ruby. pledge(2) allows a program to restrict the
5
+ types of operations the program can do, and unveil(2)
6
+ restricts access to the file system.
7
+
8
+ Unlike other similar systems, pledge and unveil are
9
+ designed for programs that need to use a wide variety of
10
+ operations and file access on initialization, but
8
11
  a fewer number after initialization (when user input will
9
12
  be accepted).
10
13
 
11
- pledge(2) is supported on OpenBSD 5.9+. pledge(2) supports a second
12
- argument for execpromises on OpenBSD 6.3+.
13
-
14
- == Usage
14
+ == pledge
15
15
 
16
16
  First, you need to require the library
17
17
 
@@ -53,8 +53,6 @@ in other classes:
53
53
  Object.send(:include, Pledge)
54
54
  pledge("rpath")
55
55
 
56
- == Options
57
-
58
56
  See the pledge(2) man page for a description of the allowed
59
57
  promises in the strings passed to +Pledge.pledge+.
60
58
 
@@ -63,6 +61,69 @@ promise is added automatically to the current process's promises,
63
61
  as ruby does not function without it, but it is not added to
64
62
  the execpromises (as you can execute non-ruby programs).
65
63
 
64
+ == unveil
65
+
66
+ First, you need to require the library
67
+
68
+ require 'unveil'
69
+
70
+ Then you can use +Pledge.unveil+ as the interface to the unveil(2)
71
+ system call. You pass +Pledge.unveil+ a hash of paths and permissions,
72
+ for those paths, and it calls unveil(2) with the path and permissions
73
+ for each entry.
74
+
75
+ The permissions should be a string with the following characters:
76
+
77
+ r :: Allow read access to existing files and directories
78
+ w :: Allow write access to existing files and directories
79
+ x :: Allow execute access to programs
80
+ c :: Allow create access for new files and directories
81
+
82
+ You can use the empty string as permissions if you want to allow no access
83
+ to the given path, even if you have granted some access to a folder above
84
+ the given folder. You can use a value of +:gem+ to allow read access to
85
+ the directory for the gem specified by the key.
86
+
87
+ +Pledge.unveil+ locks the file system access to the specified paths. If
88
+ you want to specify which paths to allow in multiple places in your
89
+ program, use +Pledge.unveil_without_lock+ for the initial calls and
90
+ +Pledge.unveil+ for the final call.
91
+
92
+ If +Pledge.unveil+ is called with an empty hash, it adds an unveil of +/+
93
+ with no permissions, which denies all access to the file system if
94
+ +unveil_without_lock+ was not called previously with paths.
95
+
96
+ Example:
97
+
98
+ Pledge.unveil(
99
+ '/home/foo/bar' => 'r',
100
+ '/home/foo/bar/data' => 'rwc',
101
+ '/bin' => 'x',
102
+ '/home/foo/bar/secret' => '',
103
+ 'rack' => :gem
104
+ )
105
+
106
+ The value of :gem is mostly mostly needed if the gem uses autoload or
107
+ other forms of runtime requires. This allows read access to
108
+ all files in the gem's folder, not just the gem's require paths,
109
+ so it works correctly for gems that access data (e.g. templates)
110
+ outside of the gem's require paths.
111
+
112
+ If you plan to use pledge and unveil together, you should
113
+ unveil before pledging, unless you use the +unveil+
114
+ promise when pledging.
115
+
116
+ === Issues with unveil and File.realpath
117
+
118
+ +Pledge.unveil+ does not work with +File.realpath+ on Ruby <2.7.
119
+ The Ruby ports officially supported by OpenBSD have had support to
120
+ allow them to work together backported, as long as you are running
121
+ OpenBSD 6.6+ (or 6.5-current after July 2019). As +require+ uses
122
+ +File.realpath+, this means in most cases where you would want to
123
+ use the +:gem+ support, it will not actually work correctly unless
124
+ you are using Ruby 2.7+ or an OpenBSD package with the backported
125
+ support.
126
+
66
127
  == Reporting issues/bugs
67
128
 
68
129
  This library uses GitHub Issues for tracking issues/bugs:
@@ -81,7 +142,7 @@ To get a copy:
81
142
 
82
143
  == Requirements
83
144
 
84
- * OpenBSD 5.9+
145
+ * OpenBSD 5.9+ (6.4+ for unveil, but 6.6+ recommended)
85
146
  * ruby 1.8.7+
86
147
  * rake-compiler (if compiling)
87
148
 
data/Rakefile CHANGED
@@ -1,13 +1,7 @@
1
1
  require "rake"
2
2
  require "rake/clean"
3
3
 
4
- CLEAN.include %w'**.rbc rdoc'
5
-
6
- desc "Do a full cleaning"
7
- task :distclean do
8
- CLEAN.include %w'tmp pkg tame*.gem lib/*.so'
9
- Rake::Task[:clean].invoke
10
- end
4
+ CLEAN.include %w'**.rbc rdoc lib/*.so tmp'
11
5
 
12
6
  desc "Build the gem"
13
7
  task :package do
@@ -1,5 +1,7 @@
1
1
  require 'mkmf'
2
- have_header 'pledge'
2
+ have_header 'unistd.h'
3
+ have_func('pledge') || raise("pledge(2) not present, cannot built extension")
4
+ have_func('unveil')
3
5
  $CFLAGS << " -O0 -g -ggdb" if ENV['DEBUG']
4
6
  $CFLAGS << " -Wall"
5
7
  create_makefile("pledge")
@@ -5,6 +5,7 @@
5
5
  static VALUE ePledgeInvalidPromise;
6
6
  static VALUE ePledgePermissionIncreaseAttempt;
7
7
  static VALUE ePledgeError;
8
+ static VALUE ePledgeUnveilError;
8
9
 
9
10
  static VALUE rb_pledge(int argc, VALUE* argv, VALUE pledge_class) {
10
11
  VALUE promises = Qnil;
@@ -44,6 +45,37 @@ static VALUE rb_pledge(int argc, VALUE* argv, VALUE pledge_class) {
44
45
  return Qnil;
45
46
  }
46
47
 
48
+ #ifdef HAVE_UNVEIL
49
+ static VALUE check_unveil(const char * path, const char * perm) {
50
+ if (unveil(path, perm) != 0) {
51
+ switch(errno) {
52
+ case EINVAL:
53
+ rb_raise(ePledgeUnveilError, "invalid permissions value");
54
+ case EPERM:
55
+ rb_raise(ePledgeUnveilError, "attempt to increase permissions, path not accessible, or unveil already locked");
56
+ case E2BIG:
57
+ rb_raise(ePledgeUnveilError, "per-process limit for unveiled paths reached");
58
+ case ENOENT:
59
+ rb_raise(ePledgeUnveilError, "directory in the path does not exist");
60
+ default:
61
+ rb_raise(ePledgeUnveilError, "unveil error");
62
+ }
63
+ }
64
+
65
+ return Qnil;
66
+ }
67
+
68
+ static VALUE rb_unveil(VALUE pledge_class, VALUE path, VALUE perm) {
69
+ SafeStringValue(path);
70
+ SafeStringValue(perm);
71
+ return check_unveil(RSTRING_PTR(path), RSTRING_PTR(perm));
72
+ }
73
+
74
+ static VALUE rb_finalize_unveil(VALUE pledge_class) {
75
+ return check_unveil(NULL, NULL);
76
+ }
77
+ #endif
78
+
47
79
  void Init_pledge(void) {
48
80
  VALUE cPledge;
49
81
  cPledge = rb_define_module("Pledge");
@@ -52,4 +84,10 @@ void Init_pledge(void) {
52
84
  ePledgeError = rb_define_class_under(cPledge, "Error", rb_eStandardError);
53
85
  ePledgeInvalidPromise = rb_define_class_under(cPledge, "InvalidPromise", ePledgeError);
54
86
  ePledgePermissionIncreaseAttempt = rb_define_class_under(cPledge, "PermissionIncreaseAttempt", ePledgeError);
87
+
88
+ #ifdef HAVE_UNVEIL
89
+ rb_define_private_method(cPledge, "_unveil", rb_unveil, 2);
90
+ rb_define_private_method(cPledge, "_finalize_unveil!", rb_finalize_unveil, 0);
91
+ ePledgeUnveilError = rb_define_class_under(cPledge, "UnveilError", rb_eStandardError);
92
+ #endif
55
93
  }
@@ -0,0 +1,64 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'pledge'
4
+ raise LoadError, "unveil not supported" unless Pledge.respond_to?(:_unveil, true)
5
+
6
+ module Pledge
7
+ # Limit access to the file system using unveil(2). +paths+ should be a hash
8
+ # where keys are paths and values are the access permissions for that path. Each
9
+ # value should be a string with the following characters specifying what
10
+ # permissions are allowed:
11
+ #
12
+ # r :: Allow read access to existing files and directories
13
+ # w :: Allow write access to existing files and directories
14
+ # c :: Allow create/delete access for new files and directories
15
+ # x :: Allow execute access to programs
16
+ #
17
+ # You can use the empty string as permissions if you want to allow no access
18
+ # to the given path, even if you have granted some access to a folder above
19
+ # the given folder. You can use a value of +:gem+ to allow read access to
20
+ # the directory for the gem specified by the key.
21
+ #
22
+ # If called with an empty hash, adds an unveil of +/+ with no permissions,
23
+ # which denies all access to the file system if +unveil_without_lock+
24
+ # was not called previously.
25
+ def unveil(paths)
26
+ if paths.empty?
27
+ paths = {'/'=>''}
28
+ end
29
+
30
+ unveil_without_lock(paths)
31
+ _finalize_unveil!
32
+ end
33
+
34
+ # Same as unveil, but allows for future calls to unveil or unveil_without_lock.
35
+ def unveil_without_lock(paths)
36
+ paths = Hash[paths]
37
+
38
+ paths.to_a.each do |path, perm|
39
+ unless path.is_a?(String)
40
+ raise UnveilError, "unveil path is not a string: #{path.inspect}"
41
+ end
42
+
43
+ case perm
44
+ when :gem
45
+ unless spec = Gem.loaded_specs[path]
46
+ raise UnveilError, "cannot unveil gem #{path} as it is not loaded"
47
+ end
48
+
49
+ paths.delete(path)
50
+ paths[spec.full_gem_path] = 'r'
51
+ when String
52
+ # nothing to do
53
+ else
54
+ raise UnveilError, "unveil permission is not a string: #{perm.inspect}"
55
+ end
56
+ end
57
+
58
+ paths.each do |path, perm|
59
+ _unveil(path, perm)
60
+ end
61
+
62
+ nil
63
+ end
64
+ end
@@ -41,7 +41,7 @@ describe "Pledge.pledge" do
41
41
  proc{Pledge.pledge("foo")}.must_raise Pledge::InvalidPromise
42
42
  end
43
43
 
44
- it "should raise a Pledge::PermissionIncreaseAttempt if attempting to increase permissinos" do
44
+ it "should raise a Pledge::PermissionIncreaseAttempt if attempting to increase permissions" do
45
45
  pledged("begin; Pledge.pledge('rpath'); rescue Pledge::PermissionIncreaseAttempt; exit 0; end; exit 1")
46
46
  end
47
47
 
@@ -121,9 +121,9 @@ describe "Pledge.pledge" do
121
121
  end
122
122
 
123
123
  it "should handle both promises and execpromises arguments" do
124
- execpledged("proc exec rpath", "stdio rpath", "exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE') ? 0 : 1)").must_equal true
125
- execpledged("proc exec", "stdio rpath", "$stderr.reopen('/dev/null', 'w'); exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE') ? 0 : 1)").must_equal false
126
- execpledged("proc exec rpath", "stdio", "$stderr.reopen('/dev/null', 'w'); exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE') ? 0 : 1)").must_equal false
124
+ execpledged("proc exec rpath", "stdio rpath", "exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE'))").must_equal true
125
+ execpledged("proc exec", "stdio rpath", "$stderr.reopen('/dev/null', 'w'); exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE'))").must_equal false
126
+ execpledged("proc exec rpath", "stdio", "$stderr.reopen('/dev/null', 'w'); exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE'))").must_equal false
127
127
  end
128
128
 
129
129
  it "should handle nil arguments" do
@@ -133,6 +133,82 @@ describe "Pledge.pledge" do
133
133
  execpledged("", nil, "`cat MIT-LICENSE`").must_equal false
134
134
  execpledged(nil, "stdio rpath", "`cat MIT-LICENSE`").must_equal true
135
135
  execpledged(nil, "stdio", "File.read('MIT-LICENSE')").must_equal true
136
- execpledged(nil, "stdio", "$stderr.reopen('/dev/null', 'w'); exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE') ? 0 : 1)").must_equal false
136
+ execpledged(nil, "stdio", "$stderr.reopen('/dev/null', 'w'); exit(`cat MIT-LICENSE` == File.read('MIT-LICENSE'))").must_equal false
137
137
  end
138
138
  end
139
+
140
+ describe "Pledge.unveil" do
141
+ def unveiled(unveils, code)
142
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "Pledge.unveil(#{unveils.inspect}); #{code}")
143
+ end
144
+
145
+ test_file = "spec/#{$$}_test.rb"
146
+
147
+ after do
148
+ File.delete(test_file) if File.file?(test_file)
149
+ end
150
+
151
+ it "should handle unveiling paths" do
152
+ unveiled({}, "exit(Dir['*'].empty?)").must_equal true
153
+ unveiled({'.'=>'r'}, "exit(!Dir['*'].empty?)").must_equal true
154
+
155
+ test_read = "exit(((File.read('MIT-LICENSE'); true) rescue false))"
156
+ unveiled({'.'=>'w'}, test_read).must_equal false
157
+ unveiled({'.'=>'r'}, test_read).must_equal true
158
+ unveiled({'.'=>'r'}, "exit(((File.open('MIT-LICENSE', 'w'){}; true) rescue false))").must_equal false
159
+
160
+ %w'rwxc rwx rwc rxc rx rw rc'.each do |perm|
161
+ unveiled({'.'=>perm}, test_read).must_equal true
162
+ end
163
+
164
+ %w'wxc wx wc xc x w c'.each do |perm|
165
+ unveiled({'.'=>perm}, test_read).must_equal false
166
+ end
167
+
168
+ unveiled({'MIT-LICENSE'=>'r'}, test_read).must_equal true
169
+ unveiled({'Rakefile'=>'r'}, test_read).must_equal false
170
+ unveiled({'.'=>'r', 'MIT-LICENSE'=>''}, test_read).must_equal false
171
+ unveiled({}, "Pledge.unveil{} rescue exit(1)").must_equal false
172
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "Pledge.unveil_without_lock({'.'=>'r'}); Pledge.unveil({}); #{test_read}").must_equal true
173
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "Pledge.unveil('foo/bar'=>'r') rescue exit(1)").must_equal false
174
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "Pledge.send(:_unveil, '.', 'f') rescue exit(1)").must_equal false
175
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "Pledge.unveil({1=>'s'}) rescue exit(1)").must_equal false
176
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "Pledge.unveil({'s'=>1}) rescue exit(1)").must_equal false
177
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "Pledge.send(:_unveil, 1, 'r') rescue exit(1)").must_equal false
178
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "Pledge.send(:_unveil, '.', 1) rescue exit(1)").must_equal false
179
+ end
180
+
181
+ it "should handle require after unveil with read access after removing from $LOADED_FEATURES" do
182
+ [File.join('.', test_file), File.join(Dir.pwd, test_file)].each do |f|
183
+ f = f.inspect
184
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', <<-END).must_equal true
185
+ File.open(#{f}, 'w'){|f| f.write '1'}
186
+ require #{f}
187
+ Pledge.unveil('spec'=>'r')
188
+ $LOADED_FEATURES.delete #{f}
189
+ require #{f}
190
+ END
191
+ end
192
+ end
193
+
194
+ it "should handle :gem value to unveil gems" do
195
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "$stderr.reopen('/dev/null', 'w'); require 'rubygems'; gem 'minitest'; require 'minitest'; Pledge.unveil({}); require 'minitest/benchmark' rescue exit(1)").must_equal false
196
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "require 'rubygems'; gem 'minitest'; require 'minitest'; Pledge.unveil('minitest'=>:gem); require 'minitest/benchmark' rescue (p $!; puts $!.backtrace; exit(1))").must_equal true
197
+
198
+ system(RUBY, '-I', 'lib', '-r', 'unveil', '-e', "Pledge.unveil('gadzooks!!!'=>:gem) rescue exit(1)").must_equal false
199
+ end
200
+
201
+ it "should need create and write access for writing new files, and create access for removing files" do
202
+ unveiled({'.'=>'w'}, "File.open(#{test_file.inspect}, 'w'){|f| f.write '1'} rescue exit(1)").must_equal false
203
+ File.file?(test_file).must_equal false
204
+ unveiled({'.'=>'c'}, "File.open(#{test_file.inspect}, 'w'){|f| f.write '1'} rescue exit(1)").must_equal false
205
+ File.file?(test_file).must_equal false
206
+ unveiled({'.'=>'cw'}, "File.open(#{test_file.inspect}, 'w'){|f| f.write '1'}").must_equal true
207
+ File.read(test_file).must_equal '1'
208
+
209
+ unveiled({'.'=>'w'}, "File.delete(#{test_file.inspect}) rescue exit(1)").must_equal false
210
+ File.read(test_file).must_equal '1'
211
+ unveiled({'.'=>'c'}, "File.delete(#{test_file.inspect})").must_equal true
212
+ File.file?(test_file).must_equal false
213
+ end
214
+ end if Pledge.respond_to?(:_unveil, true)
metadata CHANGED
@@ -1,23 +1,22 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pledge
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-25 00:00:00.000000000 Z
11
+ date: 2019-07-07 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
- pledge exposes OpenBSD's pledge(2) system call to ruby, allowing a
15
- program to restrict the types of operations the program
16
- can do after that point. Unlike other similar systems,
17
- pledge is specifically designed for programs that need to
18
- use a wide variety of operations on initialization, but
19
- a fewer number after initialization (when user input will
20
- be accepted).
14
+ pledge exposes OpenBSD's pledge(2) and unveil(2) system calls to Ruby, allowing
15
+ a program to restrict the types of operations the program can do, and the file
16
+ system access the program has, after the point of call. Unlike other similar
17
+ systems, pledge and unveil are specifically designed for programs that need to
18
+ use a wide variety of operations on initialization, but a fewer number after
19
+ initialization (when user input will be accepted).
21
20
  email: code@jeremyevans.net
22
21
  executables: []
23
22
  extensions:
@@ -33,6 +32,7 @@ files:
33
32
  - Rakefile
34
33
  - ext/pledge/extconf.rb
35
34
  - ext/pledge/pledge.c
35
+ - lib/unveil.rb
36
36
  - spec/pledge_spec.rb
37
37
  homepage: https://github.com/jeremyevans/ruby-pledge
38
38
  licenses:
@@ -44,7 +44,7 @@ rdoc_options:
44
44
  - "--line-numbers"
45
45
  - "--inline-source"
46
46
  - "--title"
47
- - 'pledge: restrict system operations on OpenBSD'
47
+ - 'pledge: restrict system operations and file system access on OpenBSD'
48
48
  - "--main"
49
49
  - README.rdoc
50
50
  require_paths:
@@ -63,5 +63,5 @@ requirements: []
63
63
  rubygems_version: 3.0.3
64
64
  signing_key:
65
65
  specification_version: 4
66
- summary: Restrict system operations on OpenBSD
66
+ summary: Restrict system operations and file system access on OpenBSD
67
67
  test_files: []