cuba 3.3.0 → 3.4.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: 51a2e9df839f23563e53c9a4a7bc93657b55a781
4
- data.tar.gz: 43432f0a85b2133cc1b052dd2f8ac4bf88eea0a2
3
+ metadata.gz: 33341fdef71be26c9506be1f85b0d223721baaf3
4
+ data.tar.gz: f78025b8496068b945a4095a51115dbb9d939c08
5
5
  SHA512:
6
- metadata.gz: a8e53ef5c9a833d709bd095138aeb9b57be355d3758428c396e864a4b81d381615594a0c33977cdd960b727588178ba1ef8d2e87bec379ca84054abcd1d7f173
7
- data.tar.gz: aa4fb0f2247bb8568fba9b38374a423ddee945972fe4143d97d5f488bdad2a08832c2581bbdc51d8c3726446cf5daf4f9a715215df713c911f44629e05d39e46
6
+ metadata.gz: 933e16d3505eca916f1d86fbb36aaf2b58fc87256272b326d0ca52afcbd3dbc699e19bae18a4f5191d05194c53b12b882983e64240ab493691445613db2c327f
7
+ data.tar.gz: fa31afdad6c4f18337c88f5a4fd896f6585d972c53a92ca2e469bbe8eafcbe53927246e99fa46e09ce0d51a28d36628aa702cabb74d98215b3e6bec06e6157e9
data/.gems CHANGED
@@ -1,4 +1,4 @@
1
- cutest -v 1.2.1
2
- rack -v 1.5.2
1
+ cutest -v 1.2.2
2
+ rack -v 1.6.0
3
3
  tilt -v 2.0.1
4
- rack-test -v 0.6.2
4
+ rack-test -v 0.6.3
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ 3.4.0
2
+
3
+ * Add `Cuba::Safe` plugin. This plugin contains security related
4
+ defaults.
5
+
1
6
  3.3.0
2
7
 
3
8
  * Restrict when to add the default content type.
data/LICENSE CHANGED
@@ -1,4 +1,8 @@
1
- Copyright (c) 2010, 2011 Michel Martens, Damian Janowski and Cyril David
1
+ Copyright (C) 2008-2009 Christian Neukirchen
2
+ Copyright (c) 2010-2015 Michel Martens
3
+ Copyright (c) 2010-2015 Damian Janowski
4
+ Copyright (c) 2010-2015 Cyril David
5
+ Copyright (c) 2013-2015 Francesco Rodríguez
2
6
 
3
7
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
8
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -217,7 +217,7 @@ Cuba.define do
217
217
  on get do
218
218
  on "hello" do
219
219
  on root do
220
- res.write "hello world"
220
+ res.write "hello world"
221
221
  end
222
222
  end
223
223
  end
@@ -274,36 +274,120 @@ end
274
274
  Security
275
275
  --------
276
276
 
277
- The favorite security layer for Cuba is
278
- [Rack::Protection][rack-protection]. It is not included by default
277
+ The most important security consideration is to use `https` for all
278
+ requests. If that's not the case, any attempt to secure the application
279
+ could be in vain. The rest of this section assumes `https` is
280
+ enforced.
281
+
282
+ When building a web application, you need to include a security
283
+ layer. Cuba ships with the `Cuba::Safe` plugin, which applies several
284
+ security related headers to prevent attacks like clickjacking and
285
+ cross-site scripting, among others. It is not included by default
279
286
  because there are legitimate uses for plain Cuba (for instance,
280
287
  when designing an API).
281
288
 
282
- If you are building a web application, by all means make sure
283
- to include a security layer. As it is the convention for unsafe
284
- operations, only POST, PUT and DELETE requests are monitored.
289
+ Here's how to include it:
290
+
291
+ ```ruby
292
+ Cuba.plugin(Cuba::Safe)
293
+ ```
285
294
 
286
295
  You should also always set a session secret to some undisclosed
287
296
  value. Keep in mind that the content in the session cookie is
288
297
  *not* encrypted.
289
298
 
290
- [rack-protection]: https://github.com/rkh/rack-protection
291
-
292
299
  ``` ruby
300
+ Cuba.use(Rack::Session::Cookie, :secret => "__a_very_long_string__")
301
+ ```
302
+
303
+ In the end, your application should look like this:
304
+
305
+ ```ruby
293
306
  require "cuba"
294
- require "rack/protection"
295
307
 
296
308
  Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"
297
- Cuba.use Rack::Protection
298
- Cuba.use Rack::Protection::RemoteReferrer
309
+
310
+ Cuba.plugin Cuba::Safe
299
311
 
300
312
  Cuba.define do
313
+ on csrf.unsafe? do
314
+ csrf.reset!
315
+
316
+ res.status = 403
317
+ res.write("Not authorized")
318
+
319
+ halt(res.finish)
320
+ end
301
321
 
302
322
  # Now your app is protected against a wide range of attacks.
303
323
  ...
304
324
  end
305
325
  ```
306
326
 
327
+ The `Cuba::Safe` plugin is composed of two modules:
328
+
329
+ * `Cuba::Safe::SecureHeaders`
330
+ * `Cuba::Safe::CSRF`
331
+
332
+ You can include them individually, but while the modularity is good
333
+ for development, it's very common to use them in tandem. As that's
334
+ the normal use case, including `Cuba::Safe` is the preferred way.
335
+
336
+ Cross-Site Request Forgery
337
+ --------------------------
338
+
339
+ The `Cuba::Safe::CSRF` plugin provides a `csrf` object with the
340
+ following methods:
341
+
342
+ * `token`: the current security token.
343
+ * `reset!`: forces the token to be recreated.
344
+ * `safe?`: returns `true` if the request is safe.
345
+ * `unsafe?`: returns `true` if the request is unsafe.
346
+ * `form_tag`: returns a string with the `csrf_token` hidden input tag.
347
+ * `meta_tag`: returns a string with the `csrf_token` meta tag.
348
+
349
+ Here's an example of how to use it:
350
+
351
+ ```ruby
352
+ Cuba.plugin(Cuba::Safe)
353
+
354
+ Cuba.define do
355
+ on csrf.unsafe? do
356
+ csrf.reset!
357
+
358
+ res.status = 403
359
+ res.write("Not authorized")
360
+
361
+ halt(res.finish)
362
+ end
363
+
364
+ # Here comes the rest of your application
365
+ # ...
366
+ end
367
+ ```
368
+
369
+ You have to include `csrf.form_tag` in your forms and `csrf.meta_tag`
370
+ among your meta tags. Here's an example that assumes you are using
371
+ `Cuba::Mote` from `cuba-contrib`:
372
+
373
+ ```html
374
+ <!DOCTYPE html>
375
+ <html>
376
+ <head>
377
+ {{ this.csrf.meta_tag }}
378
+ ...
379
+ </head>
380
+ ...
381
+ <body>
382
+ <form action="/foo" method="POST">
383
+ {{ this.csrf.form_tag }}
384
+ ...
385
+ </form>
386
+ ...
387
+ </body>
388
+ </html>
389
+ ```
390
+
307
391
  HTTP Verbs
308
392
  ----------
309
393
 
@@ -370,7 +454,7 @@ In the second case, the substring `:id` gets replaced by `([^\\/]+)` and the
370
454
  string becomes `"users/([^\\/]+)"` before performing the match, thus it reverts
371
455
  to the first form we saw.
372
456
 
373
- In the third case, the symbol ––no matter what it says––gets replaced
457
+ In the third case, the symbol ––no matter what it says––gets replaced
374
458
  by `"([^\\/]+)"`, and again we are in presence of case 1.
375
459
 
376
460
  The fourth case, again, reverts to the basic matcher: it generates the string
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "cuba"
3
- s.version = "3.3.0"
3
+ s.version = "3.4.0"
4
4
  s.summary = "Microframework for web applications."
5
5
  s.description = "Cuba is a microframework for web applications."
6
6
  s.authors = ["Michel Martens"]
@@ -8,8 +8,8 @@ class Cuba
8
8
  attr :body
9
9
  attr :headers
10
10
 
11
- def initialize(status = nil, headers = {})
12
- @status = status
11
+ def initialize(headers = {})
12
+ @status = nil
13
13
  @headers = headers
14
14
  @body = []
15
15
  @length = 0
@@ -114,7 +114,7 @@ class Cuba
114
114
  def call!(env)
115
115
  @env = env
116
116
  @req = settings[:req].new(env)
117
- @res = settings[:res].new
117
+ @res = settings[:res].new(settings[:default_headers].dup)
118
118
 
119
119
  # This `catch` statement will either receive a
120
120
  # rack response tuple via a `halt`, or will
@@ -339,7 +339,51 @@ class Cuba
339
339
  def halt(response)
340
340
  throw :halt, response
341
341
  end
342
+
343
+ # Adds ability to pass information to a nested Cuba application.
344
+ # It receives two parameters: a hash that represents the passed
345
+ # information and a block. The #vars method is used to retrieve
346
+ # a hash with the passed information.
347
+ #
348
+ # class Platforms < Cuba
349
+ # define do
350
+ # platform = vars[:platform]
351
+ #
352
+ # on default do
353
+ # res.write(platform) # => "heroku" or "salesforce"
354
+ # end
355
+ # end
356
+ # end
357
+ #
358
+ # Cuba.define do
359
+ # on "(heroku|salesforce)" do |platform|
360
+ # with(platform: platform) do
361
+ # run(Platforms)
362
+ # end
363
+ # end
364
+ # end
365
+ #
366
+ def with(dict = {})
367
+ old, env["cuba.vars"] = vars, vars.merge(dict)
368
+ yield
369
+ ensure
370
+ env["cuba.vars"] = old
371
+ end
372
+
373
+ # Returns a hash with the information set by the #with method.
374
+ #
375
+ # with(role: "admin", site: "main") do
376
+ # on default do
377
+ # res.write(vars.inspect)
378
+ # end
379
+ # end
380
+ # # => '{:role=>"admin", :site=>"main"}'
381
+ #
382
+ def vars
383
+ env["cuba.vars"] ||= {}
384
+ end
342
385
  end
343
386
 
344
387
  Cuba.settings[:req] = Rack::Request
345
388
  Cuba.settings[:res] = Cuba::Response
389
+ Cuba.settings[:default_headers] = {}
@@ -13,6 +13,7 @@ class Cuba
13
13
  end
14
14
 
15
15
  def render(template, locals = {}, layout = settings[:render][:layout])
16
+ res.headers["Content-Type"] ||= "text/html; charset=utf-8"
16
17
  res.write(view(template, locals, layout))
17
18
  end
18
19
 
@@ -0,0 +1,23 @@
1
+ require_relative "safe/csrf"
2
+ require_relative "safe/secure_headers"
3
+
4
+ class Cuba
5
+ # == Cuba::Safe
6
+ #
7
+ # This plugin contains security related features for Cuba
8
+ # applications. It takes ideas from secureheaders[1].
9
+ #
10
+ # == Usage
11
+ #
12
+ # require "cuba"
13
+ # require "cuba/safe"
14
+ #
15
+ # Cuba.plugin(Cuba::Safe)
16
+ #
17
+ module Safe
18
+ def self.setup(app)
19
+ app.plugin(Safe::SecureHeaders)
20
+ app.plugin(Safe::CSRF)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,47 @@
1
+ class Cuba
2
+ module Safe
3
+ module CSRF
4
+ def csrf
5
+ @csrf ||= Cuba::Safe::CSRF::Helper.new(req)
6
+ end
7
+
8
+ class Helper
9
+ attr :req
10
+
11
+ def initialize(req)
12
+ @req = req
13
+ end
14
+
15
+ def token
16
+ session[:csrf_token] ||= SecureRandom.base64(32)
17
+ end
18
+
19
+ def reset!
20
+ session.delete(:csrf_token)
21
+ end
22
+
23
+ def safe?
24
+ return req.get? || req.head? ||
25
+ req[:csrf_token] == token ||
26
+ req.env["HTTP_X_CSRF_TOKEN"] == token
27
+ end
28
+
29
+ def unsafe?
30
+ return !safe?
31
+ end
32
+
33
+ def form_tag
34
+ return %Q(<input type="hidden" name="csrf_token" value="#{ token }">)
35
+ end
36
+
37
+ def meta_tag
38
+ return %Q(<meta name="csrf_token" content="#{ token }">)
39
+ end
40
+
41
+ def session
42
+ return req.env["rack.session"]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ # == Secure HTTP Headers
2
+ #
3
+ # This plugin will automatically apply several headers that are
4
+ # related to security. This includes:
5
+ #
6
+ # - HTTP Strict Transport Security (HSTS) [2].
7
+ # - X-Frame-Options [3].
8
+ # - X-XSS-Protection [4].
9
+ # - X-Content-Type-Options [5].
10
+ # - X-Download-Options [6].
11
+ # - X-Permitted-Cross-Domain-Policies [7].
12
+ #
13
+ # == References
14
+ #
15
+ # [1]: https://github.com/twitter/secureheaders
16
+ # [2]: https://tools.ietf.org/html/rfc6797
17
+ # [3]: https://tools.ietf.org/html/draft-ietf-websec-x-frame-options-02
18
+ # [4]: http://msdn.microsoft.com/en-us/library/dd565647(v=vs.85).aspx
19
+ # [5]: http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx
20
+ # [6]: http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx
21
+ # [7]: https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html
22
+ #
23
+ class Cuba
24
+ module Safe
25
+ module SecureHeaders
26
+ HEADERS = {
27
+ "X-Content-Type-Options" => "nosniff",
28
+ "X-Download-Options" => "noopen",
29
+ "X-Frame-Options" => "SAMEORIGIN",
30
+ "X-Permitted-Cross-Domain-Policies" => "none",
31
+ "X-XSS-Protection" => "1; mode=block",
32
+ "Strict-Transport-Security" => "max-age=631138519; includeSubdomains; preload"
33
+ }
34
+
35
+ def self.setup(app)
36
+ app.settings[:default_headers].merge!(HEADERS)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,139 @@
1
+ require_relative "helper"
2
+ require "cuba/safe/csrf"
3
+ require "cuba/test"
4
+
5
+ def assert_no_raise
6
+ yield
7
+ success
8
+ end
9
+
10
+ class UnsafeRequest < RuntimeError; end
11
+
12
+ scope do
13
+ setup do
14
+ Cuba.reset!
15
+
16
+ Cuba.use(Rack::Session::Cookie, secret: "_this_must_be_secret")
17
+ Cuba.plugin(Cuba::Safe::CSRF)
18
+ end
19
+
20
+ test "safe http methods" do
21
+ Cuba.define do
22
+ raise UnsafeRequest if csrf.unsafe?
23
+ end
24
+
25
+ assert_no_raise do
26
+ get "/"
27
+ head "/"
28
+ end
29
+ end
30
+
31
+ test "invalid csrf param" do
32
+ Cuba.define do
33
+ if csrf.unsafe?
34
+ csrf.reset!
35
+ end
36
+
37
+ res.write(csrf.token)
38
+ end
39
+
40
+ get "/"
41
+
42
+ old_token = last_response.body
43
+
44
+ post "/", "csrf_token" => "nonsense"
45
+
46
+ new_token = last_response.body
47
+
48
+ assert(old_token != new_token)
49
+ end
50
+
51
+ test "valid csrf param" do
52
+ Cuba.define do
53
+ raise unless csrf.safe?
54
+
55
+ on get do
56
+ res.write(csrf.token)
57
+ end
58
+
59
+ on post do
60
+ res.write("safe")
61
+ end
62
+ end
63
+
64
+ get "/"
65
+
66
+ csrf_token = last_response.body
67
+
68
+ assert(!csrf_token.empty?)
69
+
70
+ assert_no_raise do
71
+ post "/", "csrf_token" => csrf_token
72
+ end
73
+ end
74
+
75
+ test "http header" do
76
+ csrf_token = SecureRandom.hex(32)
77
+
78
+ Cuba.define do
79
+ session[:csrf_token] = csrf_token
80
+ raise if csrf.unsafe?
81
+ end
82
+
83
+ assert_no_raise do
84
+ post "/", {}, { "HTTP_X_CSRF_TOKEN" => csrf_token }
85
+ end
86
+ end
87
+
88
+ test "sub app raises too" do
89
+ class App < Cuba
90
+ define do
91
+ on post do
92
+ res.write("unsafe")
93
+ end
94
+ end
95
+ end
96
+
97
+ Cuba.define do
98
+ raise UnsafeRequest unless csrf.safe?
99
+
100
+ on "app" do
101
+ run(App)
102
+ end
103
+ end
104
+
105
+ assert_raise(UnsafeRequest) do
106
+ post "/app"
107
+ end
108
+ end
109
+
110
+ test "only sub app" do
111
+ class App < Cuba
112
+ define do
113
+ raise UnsafeRequest unless csrf.safe?
114
+
115
+ on post do
116
+ res.write("unsafe")
117
+ end
118
+ end
119
+ end
120
+
121
+ Cuba.define do
122
+ on "app" do
123
+ run(App)
124
+ end
125
+
126
+ on default do
127
+ res.write("safe")
128
+ end
129
+ end
130
+
131
+ assert_no_raise do
132
+ post "/"
133
+ end
134
+
135
+ assert_raise(UnsafeRequest) do
136
+ post "/app"
137
+ end
138
+ end
139
+ end
@@ -111,3 +111,18 @@ test "overrides layout" do
111
111
 
112
112
  assert_response body, ["<title>Alternative Layout: Home</title>\n<h1>Home</h1>\n<p>Hello Agent Smith</p>\n"]
113
113
  end
114
+
115
+ test "ensures content-type header is set" do
116
+ Cuba.plugin(Cuba::Render)
117
+
118
+ Cuba.define do
119
+ on default do
120
+ res.status = 403
121
+ render("about", title: "Hello Cuba")
122
+ end
123
+ end
124
+
125
+ _, headers, _ = Cuba.call({})
126
+
127
+ assert_equal("text/html; charset=utf-8", headers["Content-Type"])
128
+ end
@@ -0,0 +1,74 @@
1
+ require_relative "helper"
2
+ require "cuba/safe"
3
+
4
+ scope do
5
+ test "secure headers" do
6
+ Cuba.plugin(Cuba::Safe)
7
+
8
+ class Hello < Cuba
9
+ define do
10
+ on root do
11
+ res.write("hello")
12
+ end
13
+ end
14
+ end
15
+
16
+ Cuba.define do
17
+ on root do
18
+ res.write("home")
19
+ end
20
+
21
+ on "hello" do
22
+ run(Hello)
23
+ end
24
+ end
25
+
26
+ secure_headers = Cuba::Safe::SecureHeaders::HEADERS
27
+
28
+ _, headers, _ = Cuba.call("PATH_INFO" => "/", "SCRIPT_NAME" => "/")
29
+ secure_headers.each do |header, value|
30
+ assert_equal(value, headers[header])
31
+ end
32
+
33
+ _, headers, _ = Cuba.call("PATH_INFO" => "/hello", "SCRIPT_NAME" => "/")
34
+ secure_headers.each do |header, value|
35
+ assert_equal(value, headers[header])
36
+ end
37
+ end
38
+
39
+ test "secure headers only in sub app" do
40
+ Cuba.settings[:default_headers] = {}
41
+
42
+ class About < Cuba
43
+ plugin(Cuba::Safe)
44
+
45
+ define do
46
+ on root do
47
+ res.write("about")
48
+ end
49
+ end
50
+ end
51
+
52
+ Cuba.define do
53
+ on root do
54
+ res.write("home")
55
+ end
56
+
57
+ on "about" do
58
+ run(About)
59
+ end
60
+ end
61
+
62
+ secure_headers = Cuba::Safe::SecureHeaders::HEADERS
63
+
64
+ _, headers, _ = Cuba.call("PATH_INFO" => "/", "SCRIPT_NAME" => "/")
65
+ secure_headers.each do |header, _|
66
+ assert(!headers.key?(header))
67
+ end
68
+
69
+ _, headers, _ = Cuba.call("PATH_INFO" => "/about", "SCRIPT_NAME" => "/")
70
+ secure_headers.each do |header, value|
71
+ assert_equal(value, headers[header])
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,42 @@
1
+ require_relative "helper"
2
+
3
+ test do
4
+ class UserPhotos < Cuba
5
+ define do
6
+ on root do
7
+ res.write "uid: %d" % vars[:user_id]
8
+ res.write "site: %s" % vars[:site]
9
+ end
10
+ end
11
+ end
12
+
13
+ class Photos < Cuba
14
+ define do
15
+ on ":id/photos" do |id|
16
+ with user_id: id do
17
+ _, _, body = UserPhotos.call(req.env)
18
+
19
+ body.each do |line|
20
+ res.write line
21
+ end
22
+ end
23
+
24
+ res.write vars.inspect
25
+ end
26
+ end
27
+ end
28
+
29
+ Cuba.define do
30
+ on "users" do
31
+ with user_id: "default", site: "main" do
32
+ run Photos
33
+ end
34
+ end
35
+ end
36
+
37
+ _, _, body = Cuba.call({ "PATH_INFO" => "/users/1001/photos",
38
+ "SCRIPT_NAME" => "" })
39
+
40
+ assert_response body, ["uid: 1001", "site: main",
41
+ '{:user_id=>"default", :site=>"main"}']
42
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cuba
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michel Martens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-19 00:00:00.000000000 Z
11
+ date: 2015-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -87,12 +87,16 @@ files:
87
87
  - lib/cuba.rb
88
88
  - lib/cuba/capybara.rb
89
89
  - lib/cuba/render.rb
90
+ - lib/cuba/safe.rb
91
+ - lib/cuba/safe/csrf.rb
92
+ - lib/cuba/safe/secure_headers.rb
90
93
  - lib/cuba/test.rb
91
94
  - makefile
92
95
  - test/accept.rb
93
96
  - test/captures.rb
94
97
  - test/composition.rb
95
98
  - test/cookie.rb
99
+ - test/csrf.rb
96
100
  - test/extension.rb
97
101
  - test/helper.rb
98
102
  - test/host.rb
@@ -109,6 +113,7 @@ files:
109
113
  - test/render.rb
110
114
  - test/root.rb
111
115
  - test/run.rb
116
+ - test/safe.rb
112
117
  - test/segment.rb
113
118
  - test/session.rb
114
119
  - test/settings.rb
@@ -126,6 +131,7 @@ files:
126
131
  - test/views/layout.mote
127
132
  - test/views/layout.str
128
133
  - test/views/test.erb
134
+ - test/with.rb
129
135
  homepage: https://github.com/soveran/cuba
130
136
  licenses:
131
137
  - MIT
@@ -146,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
152
  version: '0'
147
153
  requirements: []
148
154
  rubyforge_project:
149
- rubygems_version: 2.0.3
155
+ rubygems_version: 2.0.14
150
156
  signing_key:
151
157
  specification_version: 4
152
158
  summary: Microframework for web applications.