github-safegem 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 2
data/bin/safegem ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
+
5
+ require 'rubygems'
6
+ require 'rubygems/specification'
7
+ require 'sinatra'
8
+ require 'timeout'
9
+ require 'yaml'
10
+
11
+ post '/' do
12
+ r, w = IO.pipe
13
+
14
+ pid = nil
15
+ begin
16
+ repo = params[:repo]
17
+ data = params[:data]
18
+ tmpdir = "tmp/#{repo}"
19
+ spec = nil
20
+
21
+ Timeout::timeout(15) do
22
+ `git clone --depth 1 git://github.com/#{repo} #{tmpdir}`
23
+
24
+ pid = fork do
25
+ begin
26
+ r.close
27
+
28
+ require 'safegem/security'
29
+ require 'safegem/lazy_dir'
30
+ Dir.chdir(tmpdir) do
31
+ thread = Thread.new do
32
+ eval <<-EOE
33
+ BEGIN { # First in first out. Get this one exec'ed before the code below.
34
+ Object.class_eval do
35
+ remove_const :OrigDir rescue nil
36
+ OrigDir = Dir
37
+ remove_const :Dir
38
+ Dir = LazyDir
39
+ end
40
+ $SAFE = 3
41
+ OrigDir.set_safe_level
42
+ }
43
+ BEGIN { # This forces Ruby to ignore nested END {} blocks
44
+ begin
45
+ params = tmpdir = data = spec = repo = nil
46
+ # Pass data out using TLS
47
+ Thread.current[:spec] = (#{data})
48
+ ensure
49
+ Object.class_eval do
50
+ remove_const :Dir
51
+ Dir = OrigDir
52
+ end
53
+ end
54
+ }
55
+ EOE
56
+ end.join
57
+ Dir.set_safe_level
58
+ spec = thread[:spec]
59
+ spec.rubygems_version = Gem::RubyGemsVersion # make sure validation passes
60
+ spec.validate
61
+ end
62
+
63
+ w.write YAML.dump(spec)
64
+ rescue Object
65
+ puts $!,$@
66
+
67
+ w.write "ERROR: #$!"
68
+ end
69
+ end
70
+ w.close
71
+
72
+ Process.wait pid
73
+ r.read
74
+ end
75
+ rescue Exception
76
+ Process.kill 9, pid
77
+ puts $!,$@
78
+
79
+ "ERROR: #$!"
80
+ ensure
81
+ `rm -rf #{tmpdir}` if tmpdir
82
+ end
83
+ end
84
+
85
+ Sinatra::Application.run!
@@ -0,0 +1,39 @@
1
+ class LazyDir < Array
2
+ OrigDir = Dir
3
+
4
+ def initialize(method, args, block = nil)
5
+ @method, @args, @block = method, args, block
6
+ end
7
+
8
+ # this method is meant to be called lazily after the $SAFE has reverted to 0
9
+ def to_a
10
+ raise SecurityError unless %w([] glob).include? @method
11
+ files = OrigDir.send(@method, *@args, &@block)
12
+
13
+ # only return files within the current directory
14
+ cur_dir = File.expand_path('.') + File::SEPARATOR
15
+ files.reject do |f|
16
+ File.expand_path(f) !~ %r{^#{cur_dir}}
17
+ end
18
+ end
19
+ alias_method :to_ary, :to_a
20
+
21
+ def to_yaml(opts = {})
22
+ to_a.to_yaml(opts)
23
+ end
24
+
25
+ class << self
26
+ # these methods are meant to be called with tainted data in a $SAFE >= 3
27
+ %w(glob []).each do |method_name|
28
+ define_method method_name do |*a|
29
+ LazyDir.new method_name, a
30
+ end
31
+ end
32
+
33
+ def method_missing m, *a, &b
34
+ OrigDir.send m, *a, &b
35
+ end
36
+ end
37
+ end
38
+
39
+ LazyDir.freeze
@@ -0,0 +1,83 @@
1
+ # remove dangerous methods
2
+ %w(` system exec trap fork callcc binding).each do |method|
3
+ (class << Kernel; self; end).class_eval do
4
+ remove_method method rescue nil
5
+ undef_method method rescue nil
6
+ define_method(method) {|*a| raise SecurityError }
7
+ end
8
+ Object.class_eval do
9
+ remove_method method rescue nil
10
+ undef_method method rescue nil
11
+ define_method(method) {|*a| raise SecurityError }
12
+ end
13
+ end
14
+ Kernel.freeze
15
+
16
+ # make sure all string methods which modify self also taint the string
17
+ class String
18
+ %w(swapcase! strip! squeeze! reverse! downcase! upcase! delete! slice! replace []= <<).each do |method_name|
19
+ m = instance_method(method_name)
20
+ define_method method_name do |*args|
21
+ begin
22
+ m.bind(self).call *args
23
+ ensure
24
+ self.taint
25
+ end
26
+ end
27
+ end
28
+
29
+ %w(sub! gsub!).each do |method_name|
30
+ m = instance_method(method_name)
31
+
32
+ define_method "__real__#{method_name}" do |b, *a|
33
+ begin
34
+ m.bind(self).call(*a, &b)
35
+ ensure
36
+ self.taint
37
+ end
38
+ end
39
+
40
+ eval <<-EOF
41
+ def #{method_name} *a, &b
42
+ __real__#{method_name}(b, *a)
43
+ end
44
+ EOF
45
+ end
46
+ end
47
+
48
+
49
+
50
+ # Bug in ruby doesn't check taint when an array of globs is passed
51
+ class << Dir
52
+ # we need to track $SAFE level manually because define_method captures the $SAFE level
53
+ # of the current scope, as it would a local varaible, and of course the current scope has a $SAFE of 0
54
+ @@safe_level = 0
55
+
56
+ # since this method is defined with def instead of define_method, $SAFE will be taken from
57
+ # the calling scope which is what we want
58
+ def set_safe_level
59
+ @@safe_level = $SAFE
60
+ end
61
+
62
+ %w([] glob).each do |method_name|
63
+ m = instance_method method_name
64
+ define_method method_name do |*args|
65
+ $SAFE = @@safe_level
66
+ raise SecurityError if $SAFE >= 3 and args.flatten.any? {|a| a.tainted? }
67
+
68
+ m.bind(self).call(*args)
69
+ end
70
+ end
71
+ end
72
+
73
+ # freeze String so that the taint method can't be redefined
74
+ String.freeze
75
+
76
+ # freeze Dir so that no one can modify the @@safe_level
77
+ Dir.freeze
78
+
79
+ # freeze method classes so someone cant modify them to catch the original methods
80
+ [Method, UnboundMethod].each {|klass| klass.freeze }
81
+
82
+ # disable ObjectSpace so people cant access the original method objects
83
+ Object.send :remove_const, :ObjectSpace
data/lib/safegem.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'safegem/lazy_dir'
2
+ require 'safegem/security'
data/test/git_mock ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ `mkdir -p #{ARGV.last}`
@@ -0,0 +1,71 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
2
+ require 'safegem/lazy_dir'
3
+
4
+ require 'test/unit'
5
+ require 'fileutils'
6
+
7
+ class LazyDirTest < Test::Unit::TestCase
8
+ def setup
9
+ FileUtils.mkdir('test_glob_dir')
10
+ eval %{
11
+ Object.class_eval do
12
+ remove_const :Dir
13
+ remove_const :OrigDir rescue nil
14
+ end
15
+ ::Dir = LazyDir
16
+ ::OrigDir = LazyDir::OrigDir
17
+ }
18
+ %w(a b c d).each {|n| File.open("test_glob_dir/#{n}", 'w'){}}
19
+ end
20
+
21
+ def teardown
22
+ eval %{
23
+ Object.class_eval { remove_const :Dir }
24
+ ::Dir = OrigDir
25
+ }
26
+ FileUtils.rm_r('test_glob_dir')
27
+ end
28
+
29
+ def test_lazy_glob
30
+ assert_raises(SecurityError) do
31
+ Thread.new do
32
+ $SAFE=4
33
+ OrigDir['test_glob_dir/*']
34
+ end.join
35
+ end
36
+
37
+ lazy = Thread.new do
38
+ $SAFE=4
39
+ Dir['test_glob_dir/*']
40
+ end.value
41
+
42
+ assert_equal OrigDir['test_glob_dir/*'], lazy.to_a
43
+ assert_equal OrigDir['test_glob_dir/*'], lazy.to_ary
44
+ end
45
+
46
+ def test_lazy_glob_flags
47
+ if PLATFORM !~ /darwin/
48
+ # this will fail on osx because of fs case insensitivity, so don't run in there
49
+ assert LazyDir.glob('*/A').to_a.empty?
50
+ end
51
+ assert_equal ['test_glob_dir/a'], LazyDir.glob('*/A', File::FNM_CASEFOLD).to_a
52
+ end
53
+
54
+ def test_lazy_glob_secure
55
+ assert LazyDir['/etc/passwd'].to_a.empty?
56
+ assert LazyDir['../../*'].to_a.empty?
57
+
58
+ orig = OrigDir['./**/*'].map {|f| File.expand_path(f) }
59
+ lazy = LazyDir['../**/*'].to_a.map {|f| File.expand_path(f) }
60
+ assert_equal orig, lazy
61
+ end
62
+
63
+ def test_lazy_dir_delegates_original_dir_methods
64
+ assert Dir.pwd
65
+ dir = 'asfasdfsaf'
66
+ assert Dir.mkdir(dir)
67
+ assert File.exist?(dir)
68
+ assert Dir.rmdir(dir)
69
+ assert ! File.exist?(dir)
70
+ end
71
+ end
@@ -0,0 +1,263 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'net/http'
4
+ require 'cgi'
5
+ require 'fileutils'
6
+ require 'open4'
7
+
8
+ OUTPUT = !!ENV['SERVER_OUTPUT']
9
+ puts "safegem server output disabled, set SERVER_OUTPUT=1 to enable" if ! OUTPUT
10
+
11
+ def mv(a, b)
12
+ here = File.dirname(__FILE__)
13
+ FileUtils.mv(File.join(here, a), File.join(here, b))
14
+ end
15
+
16
+ # ensure git_mock is in place before running any of these tests
17
+ mv('git', 'git_mock') rescue nil
18
+
19
+ class SafeGemTest < Test::Unit::TestCase
20
+ def setup
21
+ here = File.dirname(__FILE__)
22
+
23
+ # put the mock git in place
24
+ mv('git_mock', 'git')
25
+
26
+ # construct the safegem command
27
+ cmd = "PATH=#{here}:$PATH ruby #{here}/../bin/safegem.rb"
28
+ cmd += " > /dev/null 2>&1" unless OUTPUT
29
+
30
+ # run safegem
31
+ @pid, _, _, _ = Open4::popen4(cmd)
32
+
33
+ # wait for server to start
34
+ Timeout::timeout(5) do
35
+ begin
36
+ TCPSocket.open('localhost', 4567) {}
37
+ server_started = true
38
+ rescue Errno::ECONNREFUSED
39
+ server_started = false
40
+ sleep 0.1
41
+ retry
42
+ end until server_started
43
+ end
44
+ end
45
+
46
+ def teardown
47
+ Process.kill("SIGHUP", @pid)
48
+ mv('git', 'git_mock')
49
+ sleep(0.5) # to let sinatra unbind the socket
50
+ end
51
+
52
+ def test_access_to_untainted_locals
53
+ %w(repo data spec params).each do |v|
54
+ assert_nil_error v
55
+ end
56
+ end
57
+
58
+ def test_timeout
59
+ puts "\ntesting 15s timeout"
60
+ begin
61
+ timeout(17) do
62
+ s = req <<-EOS
63
+ def forever
64
+ loop{}
65
+ ensure
66
+ forever
67
+ end
68
+ forever
69
+ EOS
70
+ assert_equal "ERROR: execution expired", s
71
+ end
72
+ rescue Timeout::Error
73
+ fail "timed out! no good!"
74
+ end
75
+ end
76
+
77
+ def test_legit_gemspec_works
78
+ gemspec = <<-EOS
79
+ Gem::Specification.new do |s|
80
+ s.name = "name"
81
+ s.description = 'description'
82
+ s.version = "0.0.9"
83
+ s.summary = ""
84
+ s.authors = ["coderrr"]
85
+ s.files = ['x']
86
+ end
87
+ EOS
88
+ expected_response = <<-EOS
89
+ --- !ruby/object:Gem::Specification
90
+ name: name
91
+ version: !ruby/object:Gem::Version
92
+ version: 0.0.9
93
+ platform: ruby
94
+ authors:
95
+ - coderrr
96
+ autorequire:
97
+ bindir: bin
98
+ cert_chain: []
99
+
100
+ date: 2008-10-31 00:00:00 +07:00
101
+ default_executable:
102
+ dependencies: []
103
+
104
+ description: description
105
+ email:
106
+ executables: []
107
+
108
+ extensions: []
109
+
110
+ extra_rdoc_files: []
111
+
112
+ files:
113
+ - x
114
+ has_rdoc: false
115
+ homepage:
116
+ post_install_message:
117
+ rdoc_options: []
118
+
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: "0"
126
+ version:
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: "0"
132
+ version:
133
+ requirements: []
134
+
135
+ rubyforge_project:
136
+ rubygems_version: 1.3.0
137
+ signing_key:
138
+ specification_version: 2
139
+ summary: ""
140
+ test_files: []
141
+ EOS
142
+ assert_equal clean_yaml(expected_response), clean_yaml(req(gemspec))
143
+ end
144
+
145
+ def test_gemspec_with_glob_works
146
+ system("mkdir globdir && cd globdir && touch a.rb b.rb c.txt")
147
+ gemspec = <<-EOS
148
+ Gem::Specification.new do |s|
149
+ s.name = "name"
150
+ s.description = 'description'
151
+ s.version = "0.0.9"
152
+ s.summary = ""
153
+ s.authors = ["coderrr"]
154
+ s.files = Dir.glob("globdir/**.rb")
155
+ s.test_files = Dir["globdir/**"]
156
+ # make sure array globs work with .glob and make sure glob flags work
157
+ s.executables = Dir.glob(["globdir/*.TXT", "globdir/*.RB"], File::FNM_CASEFOLD)
158
+ # make sure array globs work with [] and make sure we cant access files in parent dirs
159
+ s.extra_rdoc_files = Dir["/etc/*", "globdir"]
160
+ end
161
+ EOS
162
+ expected_response = <<-EOS
163
+ --- !ruby/object:Gem::Specification
164
+ name: name
165
+ version: !ruby/object:Gem::Version
166
+ version: 0.0.9
167
+ platform: ruby
168
+ authors:
169
+ - coderrr
170
+ autorequire:
171
+ bindir: bin
172
+ cert_chain: []
173
+
174
+
175
+ default_executable:
176
+ dependencies: []
177
+
178
+ description: description
179
+ email:
180
+ executables:
181
+ - globdir/c.txt
182
+ - globdir/a.rb
183
+ - globdir/b.rb
184
+ extensions: []
185
+
186
+ extra_rdoc_files:
187
+ - globdir
188
+ files:
189
+ - globdir/a.rb
190
+ - globdir/b.rb
191
+ has_rdoc: false
192
+ homepage:
193
+ post_install_message:
194
+ rdoc_options: []
195
+
196
+ require_paths:
197
+ - lib
198
+ required_ruby_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: "0"
203
+ version:
204
+ required_rubygems_version: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: "0"
209
+ version:
210
+ requirements: []
211
+
212
+ rubyforge_project:
213
+
214
+ signing_key:
215
+ specification_version: 2
216
+ summary: ""
217
+ test_files:
218
+ - globdir/a.rb
219
+ - globdir/b.rb
220
+ - globdir/c.txt
221
+ EOS
222
+ assert_equal clean_yaml(expected_response), clean_yaml(req(gemspec))
223
+ ensure
224
+ system("rm -rf globdir")
225
+ end
226
+
227
+ def test_tmpdir_is_destroyed
228
+ Dir.mkdir('tmp/safegem_test')
229
+ assert File.exist?('tmp/safegem_test')
230
+ req('')
231
+ assert ! File.exist?('tmp/safegem_test')
232
+ end
233
+
234
+ def test_secure_parser_begin
235
+ resp = req <<-EOS
236
+ BEGIN {require 'bogus_file'}
237
+ EOS
238
+ assert resp.include?('Insecure operation')
239
+ end
240
+
241
+ def test_secure_parser_end
242
+ resp = req <<-EOS
243
+ END {fail 'secret exit'}
244
+ EOS
245
+ assert !resp.include?('secret exit')
246
+ end
247
+
248
+ private
249
+
250
+ def clean_yaml(y)
251
+ y.strip.gsub(/ *$/m, '').sub(/^date:.+$/,'').sub(/^rubygems_version:.+$/,'')
252
+ end
253
+
254
+ def assert_nil_error(v)
255
+ assert req("#{v}.abc").include?("undefined method `abc' for nil"), "#{v} was not nil"
256
+ end
257
+
258
+ def req(data)
259
+ Net::HTTP.start 'localhost', 4567 do |h|
260
+ h.post('/', "data=#{CGI.escape data}&repo=safegem_test").body
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,58 @@
1
+ # I dont use test/unit for this because the security measures screw with it
2
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
3
+ require 'safegem/security'
4
+
5
+ def assert condition, message
6
+ raise message if ! condition
7
+
8
+ print '.'; $stdout.flush
9
+ end
10
+
11
+ def assert_raises error, message, &block
12
+ begin
13
+ yield
14
+ raise message
15
+ rescue error
16
+ print '.'; $stdout.flush
17
+ end
18
+ end
19
+
20
+ ['class Method', 'class UnboundMethod', 'module Kernel'].each do |klass|
21
+ assert_raises TypeError, "#{klass} didn't raise" do
22
+ eval("#{klass}; def x;end; end")
23
+ end
24
+ end
25
+
26
+ data = 'echo YOU SHOULDNT SEE THIS!!!!'
27
+ ['system(data)',
28
+ 'exec(data)',
29
+ 'Kernel.send(:exec,data)',
30
+ 'Object.new.exec(data)',
31
+ '`#{data}`',
32
+ 'Kernel.`(data)',
33
+ 'Kernel.send(:`,data)',
34
+ 'trap(1,lambda{})',
35
+ 'fork{}',
36
+ 'callcc{}',
37
+ 'binding'
38
+ ].each do |danger|
39
+ assert_raises SecurityError, "#{danger} worked!" do
40
+ eval danger
41
+ end
42
+ end
43
+
44
+ Thread.new do
45
+ $SAFE = 3
46
+ Dir.set_safe_level
47
+ assert_raises SecurityError, "snuck tainted string past glob" do
48
+ Dir['**','**']
49
+ Dir.glob(['**', '**'])
50
+ end
51
+ end.join
52
+ Dir.set_safe_level
53
+ Dir['**'.taint]
54
+
55
+ dirs = Dir['/**']
56
+ assert(4 == (dirs & %w(/usr /bin /home /sbin)).size, 'glob doesnt work')
57
+
58
+ puts
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: github-safegem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - PJ Hyett
8
+ - Tom Preston-Werner
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-02-10 00:00:00 -08:00
14
+ default_executable: safegem
15
+ dependencies: []
16
+
17
+ description: GitHub's safe gem eval web service
18
+ email: tom@mojombo.com
19
+ executables:
20
+ - safegem
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - VERSION.yml
27
+ - bin/safegem
28
+ - lib/safegem
29
+ - lib/safegem/lazy_dir.rb
30
+ - lib/safegem/security.rb
31
+ - lib/safegem.rb
32
+ - test/git_mock
33
+ - test/lazy_dir_test.rb
34
+ - test/safegem_test.rb
35
+ - test/security_test.rb
36
+ has_rdoc: true
37
+ homepage: http://github.com/github/safegem
38
+ post_install_message:
39
+ rdoc_options:
40
+ - --inline-source
41
+ - --charset=UTF-8
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project:
59
+ rubygems_version: 1.2.0
60
+ signing_key:
61
+ specification_version: 2
62
+ summary: GitHub's safe gem eval web service
63
+ test_files: []
64
+