codeclimate-services 1.7.0 → 1.8.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
  SHA1:
3
- metadata.gz: 08767ab67555c51f2f492eea001853158febb41b
4
- data.tar.gz: f097a06a711a69a01014dc8775bc99f2176bd028
3
+ metadata.gz: 77816c3d07a72abbd79d831e8852592b4b30b654
4
+ data.tar.gz: f8696a4c43abd9b325dc9d8baadf5033c4c8246f
5
5
  SHA512:
6
- metadata.gz: a668a902b808e680c2a0a350506313dcd87b12cbce904d634197c5a109be59a6f6009801dafd841d7df3d69fe7aa6bd2dd7a00c75d8cb067bdebf77c332340a6
7
- data.tar.gz: fe1911f20809ac655ec9b94d8cb35394173a9592bd865d683c4c39ffa63aef3c9aa2867d7e8480bdf99299e00e168d204cb418e4bf1e95f064142f7257bffd9e
6
+ metadata.gz: d6ae01e70ac2bdc798333a52500b11778b47c256c444c7cceb76316c348c532fca4bd405864d4ba12310aac91f81d1a3ae4498a3a986ceb2ef4e0959f888f1e0
7
+ data.tar.gz: 1237a7918f78505f03e166fdd34f0efe21596dc39837d41a25abe1d8b0867e5a1531205f757cf179b20efba0153f7144064604eccbff01252d1a6118b6885ff3
data/.codeclimate.yml ADDED
@@ -0,0 +1,22 @@
1
+ ---
2
+ engines:
3
+ duplication:
4
+ enabled: true
5
+ config:
6
+ languages:
7
+ - ruby
8
+ fixme:
9
+ enabled: true
10
+ rubocop:
11
+ enabled: true
12
+ exclude_fingerprints:
13
+ # Using #=== intentionally in SafeWebhook
14
+ - d1afe90be49c43e85a76bfa58f637804
15
+ # High complexity in http method due to SafeWebhook check
16
+ - f05cea2d219c0f8119eb826067a18eda
17
+ ratings:
18
+ paths:
19
+ - "**.rb"
20
+ exclude_paths:
21
+ - config/
22
+ - spec/
data/Rakefile CHANGED
@@ -1,8 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
3
 
4
- RSpec::Core::RakeTask.new(:spec) do |task|
5
- task.ruby_opts = "-W"
6
- end
4
+ RSpec::Core::RakeTask.new(:spec)
7
5
 
8
6
  task default: :spec
@@ -1,5 +1,6 @@
1
1
  require "active_support/concern"
2
2
  require "cc/service/response_check"
3
+ require "cc/service/safe_webhook"
3
4
 
4
5
  module CC::Service::HTTP
5
6
  extend ActiveSupport::Concern
@@ -38,10 +39,8 @@ module CC::Service::HTTP
38
39
  end
39
40
 
40
41
  def raw_get(url = nil, params = nil, headers = nil)
41
- http.get do |req|
42
- req.url(url) if url
42
+ http_method(:get, url, nil, headers) do |req|
43
43
  req.params.update(params) if params
44
- req.headers.update(headers) if headers
45
44
  yield req if block_given?
46
45
  end
47
46
  end
@@ -59,6 +58,11 @@ module CC::Service::HTTP
59
58
  req.headers.update(headers) if headers
60
59
  req.body = body if body
61
60
  block.call req if block
61
+
62
+ unless allow_internal_webhooks?
63
+ safe_webhook = CC::Service::SafeWebhook.new(url)
64
+ safe_webhook.validate!(req)
65
+ end
62
66
  end
63
67
  end
64
68
 
@@ -99,4 +103,9 @@ module CC::Service::HTTP
99
103
  message: "Success",
100
104
  }
101
105
  end
106
+
107
+ def allow_internal_webhooks?
108
+ var = ENV["CODECLIMATE_ALLOW_INTERNAL_WEBHOOKS"] || ""
109
+ var == "1" || var == "true"
110
+ end
102
111
  end
@@ -0,0 +1,71 @@
1
+ require "ipaddr"
2
+ require "resolv"
3
+
4
+ module CC
5
+ class Service
6
+ class SafeWebhook
7
+ InvalidWebhookURL = Class.new(StandardError)
8
+
9
+ # https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
10
+ # https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses
11
+ PRIVATE_ADDRESS_SUBNETS = [
12
+ IPAddr.new("10.0.0.0/8"),
13
+ IPAddr.new("172.16.0.0/12"),
14
+ IPAddr.new("192.168.0.0/16"),
15
+ IPAddr.new("fd00::/8"),
16
+ IPAddr.new("127.0.0.1"),
17
+ IPAddr.new("0:0:0:0:0:0:0:1"),
18
+ ].freeze
19
+
20
+ def self.getaddress(host)
21
+ @resolv ||= Resolv::DNS.new
22
+ @resolv.getaddress(host).to_s
23
+ end
24
+
25
+ def initialize(url)
26
+ @url = url
27
+ end
28
+
29
+ # Resolve the Host to an IP address, validate that it doesn't point to
30
+ # anything internal, then alter the request to be for the IP directly with
31
+ # an explicit Host header given.
32
+ #
33
+ # See http://blog.fanout.io/2014/01/27/how-to-safely-invoke-webhooks/#ip-address-blacklisting
34
+ def validate!(request)
35
+ uri = URI.parse(url)
36
+ address = self.class.getaddress(uri.host)
37
+
38
+ if internal?(address)
39
+ raise_invalid("resolves to a private IP address")
40
+ end
41
+
42
+ alter_request(request, uri, address)
43
+ rescue URI::InvalidURIError, Resolv::ResolvError, Resolv::ResolvTimeout => ex
44
+ raise_invalid(ex.message)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :url
50
+
51
+ def internal?(address)
52
+ ip_addr = IPAddr.new(address)
53
+
54
+ PRIVATE_ADDRESS_SUBNETS.any? do |subnet|
55
+ subnet === ip_addr
56
+ end
57
+ end
58
+
59
+ def alter_request(request, uri, address)
60
+ address_uri = uri.dup
61
+ address_uri.host = address
62
+ request.url(address_uri.to_s)
63
+ request.headers.update(Host: uri.host)
64
+ end
65
+
66
+ def raise_invalid(message)
67
+ raise InvalidWebhookURL, "The Webhook URL #{url} is invalid: #{message}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,5 +1,5 @@
1
1
  module CC
2
2
  module Services
3
- VERSION = "1.7.0".freeze
3
+ VERSION = "1.8.0".freeze
4
4
  end
5
5
  end
@@ -67,8 +67,10 @@ describe CC::Service::GitHubIssues, type: :service do
67
67
  end
68
68
 
69
69
  it "different base url" do
70
+ stub_resolv("example.com", "1.1.1.2")
71
+
70
72
  http_stubs.post request_url do |env|
71
- expect(env[:url].to_s).to eq("http://example.com/#{request_url}")
73
+ expect(env[:url].to_s).to eq("http://1.1.1.2/#{request_url}")
72
74
  [200, {}, '{"number": 2, "html_url": "http://foo.bar"}']
73
75
  end
74
76
 
@@ -107,8 +107,10 @@ describe CC::Service::GitHubPullRequests, type: :service do
107
107
  end
108
108
 
109
109
  it "different base url" do
110
+ stub_resolv("example.com", "1.1.1.2")
111
+
110
112
  http_stubs.post("/repos/pbrisbin/foo/statuses/#{"0" * 40}") do |env|
111
- expect(env[:url].to_s).to eq("http://example.com/repos/pbrisbin/foo/statuses/#{"0" * 40}")
113
+ expect(env[:url].to_s).to eq("http://1.1.1.2/repos/pbrisbin/foo/statuses/#{"0" * 40}")
112
114
  [422, { "x-oauth-scopes" => "gist, user, repo" }, ""]
113
115
  end
114
116
 
@@ -134,14 +134,44 @@ describe CC::Service::GitlabMergeRequests, type: :service do
134
134
  end
135
135
 
136
136
  it "different base url" do
137
+ stub_resolv("gitlab.hal.org", "1.1.1.2")
138
+
137
139
  http_stubs.post("api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}") do |env|
138
- expect(env[:url].to_s).to eq("https://gitlab.hal.org/api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}")
140
+ expect(env[:url].to_s).to eq("https://1.1.1.2/api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}")
139
141
  [404, {}, ""]
140
142
  end
141
143
 
142
144
  expect(receive_test({ base_url: "https://gitlab.hal.org" }, git_url: "ssh://git@gitlab.com/hal/hal9000.git")[:ok]).to eq(true)
143
145
  end
144
146
 
147
+ context "SafeWebhook" do
148
+ it "rewrites the request to be for the resolved IP" do
149
+ stub_resolv("my.gitlab.com", "1.1.1.2")
150
+
151
+ http_stubs.post("api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}") do |env|
152
+ expect(env[:url].to_s).to eq("https://1.1.1.2/api/v3/projects/hal%2Fhal9000/statuses/#{"0" * 40}")
153
+ expect(env[:request_headers][:Host]).to eq("my.gitlab.com")
154
+ [404, {}, ""]
155
+ end
156
+
157
+ expect(receive_test({ base_url: "https://my.gitlab.com" }, git_url: "ssh://git@gitlab.com/hal/hal9000.git")[:ok]).to eq(true)
158
+ end
159
+
160
+ it "validates that the host doesn't resolve to something internal" do
161
+ stub_resolv("my.gitlab.com", "127.0.0.1")
162
+
163
+ expect do
164
+ receive_test({ base_url: "https://my.gitlab.com" }, git_url: "")
165
+ end.to raise_error(CC::Service::SafeWebhook::InvalidWebhookURL)
166
+
167
+ stub_resolv("my.gitlab.com", "10.0.0.9")
168
+
169
+ expect do
170
+ receive_test({ base_url: "https://my.gitlab.com" }, git_url: "")
171
+ end.to raise_error(CC::Service::SafeWebhook::InvalidWebhookURL)
172
+ end
173
+ end
174
+
145
175
  private
146
176
 
147
177
  def expect_status_update(repo, commit_sha, params)
@@ -0,0 +1,61 @@
1
+ require "spec_helper"
2
+
3
+ class CC::Service
4
+ describe SafeWebhook do
5
+ describe ".getaddress" do
6
+ it "resolves the dns name to a string" do
7
+ address = SafeWebhook.getaddress("codeclimate.com")
8
+
9
+ expect(address).to be_present
10
+ expect(address).to respond_to(:start_with?)
11
+ end
12
+ end
13
+
14
+ describe "#validate!" do
15
+ context "valid webhook URLs" do
16
+ it "rewrites the request to be safe" do
17
+ stub_resolv("example.com", "2.2.2.2")
18
+
19
+ request = double(headers: double)
20
+ expect(request).to receive(:url).with("https://2.2.2.2:3000/foo")
21
+ expect(request.headers).to receive(:update).with(Host: "example.com")
22
+
23
+ safe_webhook = SafeWebhook.new("https://example.com:3000/foo")
24
+ safe_webhook.validate!(request)
25
+ end
26
+ end
27
+
28
+ context "invalid Webhook URLs" do
29
+ it "raises for invalid URL" do
30
+ allow(URI).to receive(:parse).and_raise(URI::InvalidURIError)
31
+
32
+ expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
33
+ end
34
+
35
+ it "raises for un-resolvable URL" do
36
+ allow(SafeWebhook).to receive(:getaddress).and_raise(Resolv::ResolvError)
37
+
38
+ expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
39
+ end
40
+
41
+ it "raises for localhost URLs" do
42
+ stub_resolv("example.com", "127.0.0.1")
43
+
44
+ expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
45
+ end
46
+
47
+ it "raises for internal URLs" do
48
+ stub_resolv("example.com", "10.0.0.1")
49
+
50
+ expect { validate("http://example.com") }.to raise_error(SafeWebhook::InvalidWebhookURL)
51
+ end
52
+
53
+ def validate(url)
54
+ request = double
55
+ safe_webhook = SafeWebhook.new(url)
56
+ safe_webhook.validate!(request)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
data/spec/spec_helper.rb CHANGED
@@ -34,4 +34,8 @@ RSpec.configure do |config|
34
34
  # This setting enables warnings. It's recommended, but in some cases may
35
35
  # be too noisy due to issues in dependencies.
36
36
  config.warnings = true
37
+
38
+ config.before(:each) do
39
+ stub_resolv(anything, "1.1.1.1")
40
+ end
37
41
  end
@@ -0,0 +1,10 @@
1
+ module ResolvHelper
2
+ def stub_resolv(host, address)
3
+ allow(CC::Service::SafeWebhook).to receive(:getaddress).
4
+ with(host).and_return(address)
5
+ end
6
+ end
7
+
8
+ RSpec.configure do |conf|
9
+ conf.include ResolvHelper
10
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: codeclimate-services
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Helmkamp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-15 00:00:00.000000000 Z
11
+ date: 2016-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -143,6 +143,7 @@ executables: []
143
143
  extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
+ - ".codeclimate.yml"
146
147
  - ".gitignore"
147
148
  - ".rspec"
148
149
  - ".rubocop.yml"
@@ -183,6 +184,7 @@ files:
183
184
  - lib/cc/service/invocation/with_retries.rb
184
185
  - lib/cc/service/invocation/with_return_values.rb
185
186
  - lib/cc/service/response_check.rb
187
+ - lib/cc/service/safe_webhook.rb
186
188
  - lib/cc/services.rb
187
189
  - lib/cc/services/asana.rb
188
190
  - lib/cc/services/campfire.rb
@@ -216,12 +218,14 @@ files:
216
218
  - spec/cc/service/jira_spec.rb
217
219
  - spec/cc/service/lighthouse_spec.rb
218
220
  - spec/cc/service/pivotal_tracker_spec.rb
221
+ - spec/cc/service/safe_webhook_spec.rb
219
222
  - spec/cc/service/slack_spec.rb
220
223
  - spec/cc/service/stash_pull_requests_spec.rb
221
224
  - spec/cc/service_spec.rb
222
225
  - spec/fixtures.rb
223
226
  - spec/spec_helper.rb
224
227
  - spec/support/fake_logger.rb
228
+ - spec/support/resolv_helper.rb
225
229
  - spec/support/service_context.rb
226
230
  homepage: ''
227
231
  licenses:
@@ -243,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
247
  version: '0'
244
248
  requirements: []
245
249
  rubyforge_project:
246
- rubygems_version: 2.5.1
250
+ rubygems_version: 2.4.5
247
251
  signing_key:
248
252
  specification_version: 4
249
253
  summary: Service classes for Code Climate
@@ -265,10 +269,12 @@ test_files:
265
269
  - spec/cc/service/jira_spec.rb
266
270
  - spec/cc/service/lighthouse_spec.rb
267
271
  - spec/cc/service/pivotal_tracker_spec.rb
272
+ - spec/cc/service/safe_webhook_spec.rb
268
273
  - spec/cc/service/slack_spec.rb
269
274
  - spec/cc/service/stash_pull_requests_spec.rb
270
275
  - spec/cc/service_spec.rb
271
276
  - spec/fixtures.rb
272
277
  - spec/spec_helper.rb
273
278
  - spec/support/fake_logger.rb
279
+ - spec/support/resolv_helper.rb
274
280
  - spec/support/service_context.rb