github-safegem 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+