signed_form 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -1
- data/.yardopts +0 -1
- data/Changes.md +30 -0
- data/Gemfile +2 -0
- data/README.md +152 -39
- data/app/views/signed_form/expired_form.html +25 -0
- data/lib/signed_form.rb +31 -0
- data/lib/signed_form/action_controller/permit_signed_params.rb +9 -16
- data/lib/signed_form/action_view/form_helper.rb +19 -5
- data/lib/signed_form/digest_stores.rb +7 -0
- data/lib/signed_form/digest_stores/memory_store.rb +7 -0
- data/lib/signed_form/digest_stores/null_store.rb +9 -0
- data/lib/signed_form/digestor.rb +67 -0
- data/lib/signed_form/engine.rb +5 -0
- data/lib/signed_form/errors.rb +4 -2
- data/lib/signed_form/form_builder.rb +79 -65
- data/lib/signed_form/gate_keeper.rb +50 -0
- data/lib/signed_form/hmac.rb +8 -10
- data/lib/signed_form/test_helper.rb +17 -0
- data/lib/signed_form/version.rb +2 -2
- data/signed_form.gemspec +1 -0
- data/spec/digestor_spec.rb +81 -0
- data/spec/fixtures/views/_fields.html.erb +2 -0
- data/spec/fixtures/views/form.html.erb +4 -0
- data/spec/form_builder_spec.rb +137 -30
- data/spec/hmac_spec.rb +13 -23
- data/spec/permit_signed_params_spec.rb +60 -43
- data/spec/spec_helper.rb +14 -2
- metadata +32 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ebbb64117232076f2250063ef9e6f06805df345
|
4
|
+
data.tar.gz: cb322f4dbce4c9ec4621779215c6aa7a2bbf003a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90a7f07fcd52abf71694bf4af3a4e10b64c30cb44a0f346f092b639c909ed4c6564419b0aef11bc4f92de52cb11be1c70b6faa62779c0ef1d970ca5e0820239f
|
7
|
+
data.tar.gz: 0b64c9465db9bd2ebb543be19b060cf684862da2928190f53e74de3956010692512db40fddf0b35c8983a73c0dd35803db447bd5a28f7f62da801d42ed890a30
|
data/.travis.yml
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
script: bundle exec rspec
|
2
2
|
language: ruby
|
3
|
-
before_install: gem install bundler
|
4
3
|
|
5
4
|
rvm:
|
6
5
|
- 1.9.3
|
@@ -9,4 +8,9 @@ rvm:
|
|
9
8
|
env:
|
10
9
|
- RAILS_VERSION=3-1-stable
|
11
10
|
- RAILS_VERSION=3-2-stable
|
11
|
+
- RAILS_VERSION=4-0-stable
|
12
12
|
- RAILS_VERSION=master
|
13
|
+
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- env: RAILS_VERSION=master
|
data/.yardopts
CHANGED
data/Changes.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
## 0.2.0
|
2
|
+
|
3
|
+
* Instead of using `signed_form_for` add an option for form signing to `form_for` so that signing third party builders
|
4
|
+
like SimpleForm doesn't require an adapter.
|
5
|
+
* Move configuration options to main module name-space.
|
6
|
+
* Add default options hash to be passed to `form_for`.
|
7
|
+
* Add a digestor to verify that out dated forms aren't being submitted.
|
8
|
+
* Add a test helper to make testing controllers easy.
|
9
|
+
* Only permit parameters but don't require them. Requiring them raises an exception if they're missing from the form
|
10
|
+
submission. But in cases where other parameters are sent as well and the form object may be optional this would raise
|
11
|
+
an exception that would be undesired.
|
12
|
+
* Allow all forms to be signed by default.
|
13
|
+
|
14
|
+
## 0.1.2
|
15
|
+
|
16
|
+
* Fix issues where request method was not being compared properly and request
|
17
|
+
url would not handle some potential cases leading to an erroneous rejection of
|
18
|
+
the form. [Marc Schütz, #6]
|
19
|
+
|
20
|
+
## 0.1.1
|
21
|
+
|
22
|
+
* Add some select and date/time field helpers that were not getting added to the signature [#5].
|
23
|
+
|
24
|
+
## 0.1.0
|
25
|
+
|
26
|
+
* Add `sign_destination` option to `signed_form_for`.
|
27
|
+
|
28
|
+
## 0.0.1
|
29
|
+
|
30
|
+
* Initial Release
|
data/Gemfile
CHANGED
@@ -8,6 +8,8 @@ rails_version = ENV['RAILS_VERSION'] || 'master'
|
|
8
8
|
case rails_version
|
9
9
|
when /master/
|
10
10
|
gem "rails", github: "rails/rails"
|
11
|
+
when /4-0-stable/
|
12
|
+
gem "rails", github: "rails/rails", branch: "4-0-stable"
|
11
13
|
when /3-2-stable/
|
12
14
|
gem "rails", github: "rails/rails", branch: "3-2-stable"
|
13
15
|
gem "strong_parameters"
|
data/README.md
CHANGED
@@ -3,9 +3,19 @@
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/signed_form.png)](http://badge.fury.io/rb/signed_form)
|
4
4
|
[![Build Status](https://travis-ci.org/erichmenge/signed_form.png?branch=master)](https://travis-ci.org/erichmenge/signed_form)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/erichmenge/signed_form.png)](https://codeclimate.com/github/erichmenge/signed_form)
|
6
|
+
[![Coverage Status](https://coveralls.io/repos/erichmenge/signed_form/badge.png?branch=master)](https://coveralls.io/r/erichmenge/signed_form)
|
6
7
|
|
7
8
|
SignedForm brings new convenience and security to your Rails 4 or Rails 3 application.
|
8
9
|
|
10
|
+
SignedForm is under active development. Please make sure you're reading the README associated with the version of
|
11
|
+
SignedForm you're using. Click the tag link on GitHub to switch to the version you've installed to get the correct
|
12
|
+
README.
|
13
|
+
|
14
|
+
Or be brave and bundle the gem straight from GitHub master.
|
15
|
+
|
16
|
+
A nicely displayed version of this README complete with table of contents is available
|
17
|
+
[here](http://erichmenge.com/signed_form/).
|
18
|
+
|
9
19
|
## How It Works
|
10
20
|
|
11
21
|
Traditionally, when you create a form with Rails you enter your fields using something like `f.text_field :name` and so
|
@@ -19,8 +29,8 @@ no more `attr_accessible`. It just works.
|
|
19
29
|
|
20
30
|
What this looks like:
|
21
31
|
|
22
|
-
```
|
23
|
-
<%=
|
32
|
+
```erb
|
33
|
+
<%= form_for @user, signed: true do |f| %>
|
24
34
|
<% f.add_signed_fields :zipcode, :state # Optionally add additional fields to sign %>
|
25
35
|
|
26
36
|
<%= f.text_field :name %>
|
@@ -29,9 +39,9 @@ What this looks like:
|
|
29
39
|
<% end %>
|
30
40
|
```
|
31
41
|
|
32
|
-
```
|
42
|
+
```ruby
|
33
43
|
UsersController < ApplicationController
|
34
|
-
def
|
44
|
+
def update
|
35
45
|
@user = User.find params[:id]
|
36
46
|
@user.update_attributes params[:user]
|
37
47
|
end
|
@@ -39,11 +49,21 @@ end
|
|
39
49
|
```
|
40
50
|
|
41
51
|
That's it. You're done. Need to add a field? Pop it in the form. You don't need to then update a list of attributes.
|
42
|
-
`signed_form_for` works just like the standard `form_for`.
|
43
52
|
|
44
53
|
Of course, you're free to continue using the standard `form_for`. `SignedForm` is strictly opt-in. It won't change the
|
45
54
|
way you use standard forms.
|
46
55
|
|
56
|
+
## More than just Convenience - Security
|
57
|
+
|
58
|
+
SignedForm protects you in 3 ways:
|
59
|
+
|
60
|
+
* Form fields are signed, so no alteration of the fields are allowed.
|
61
|
+
* Form actions are signed. That means a form with an action of `/admin/users/3` will not work when submitted to `/users/3`.
|
62
|
+
* Form views are digested (see below). So if you remove a field from your form, old forms will not be accepted despite
|
63
|
+
a valid signature.
|
64
|
+
|
65
|
+
The second two methods of security are optional and can be turned off globally or on a form by form basis.
|
66
|
+
|
47
67
|
## Requirements
|
48
68
|
|
49
69
|
SignedForm requires:
|
@@ -56,7 +76,9 @@ SignedForm requires:
|
|
56
76
|
|
57
77
|
Add this line to your application's Gemfile:
|
58
78
|
|
59
|
-
|
79
|
+
```ruby
|
80
|
+
gem 'signed_form'
|
81
|
+
```
|
60
82
|
|
61
83
|
And then execute:
|
62
84
|
|
@@ -70,7 +92,7 @@ If you're using Rails 4, it works out of the box.
|
|
70
92
|
You'll need to include `SignedForm::ActionController::PermitSignedParams` in the controller(s) you want to use
|
71
93
|
SignedForm with. This can be done application wide by adding the `include` to your ApplicationController.
|
72
94
|
|
73
|
-
```
|
95
|
+
```ruby
|
74
96
|
ApplicationController < ActionController::Base
|
75
97
|
include SignedForm::ActionController::PermitSignedParams
|
76
98
|
|
@@ -80,54 +102,145 @@ end
|
|
80
102
|
|
81
103
|
You'll also need to create an initializer:
|
82
104
|
|
83
|
-
|
105
|
+
```shell
|
106
|
+
$ echo "SignedForm.secret_key = '$(rake secret)'" > config/initializers/signed_form.rb
|
107
|
+
```
|
84
108
|
|
85
|
-
|
109
|
+
You'll probably want to keep this out of version control. Treat this key like you would your session secret, keep it
|
110
|
+
private.
|
86
111
|
|
87
112
|
## Support for other Builders
|
88
113
|
|
89
|
-
|
114
|
+
Any form that wraps `form_for` and the default field helpers will work with SignedForm. For example, a signed SimpleForm
|
115
|
+
might look like this:
|
90
116
|
|
91
|
-
|
117
|
+
```erb
|
118
|
+
<%= simple_form_for @user, signed: true do |f| %>
|
119
|
+
f.input :name
|
120
|
+
<% end %>
|
121
|
+
```
|
92
122
|
|
93
|
-
|
94
|
-
caveats. If a user is in process of filling out a form and you restart your server, their form will be invalidated.
|
95
|
-
You could pick a secret key using `rake secret` and put that in the initializer instead, but then in the event you
|
96
|
-
remove a field someone could still access it using the old signature if some malicious person were to keep it around.
|
123
|
+
This will create a signed form as expected.
|
97
124
|
|
98
|
-
|
99
|
-
between all the servers. The security caveat with that is that if you ever remove a field from a form without updating
|
100
|
-
that secret key, a malicious user could still access the field with the old signature. So you'll probably want to choose
|
101
|
-
a new secret in the event you remove access to an attribute in a form.
|
125
|
+
For builders that don't use the standard field helpers under the hood, you can create an adapter like this:
|
102
126
|
|
103
|
-
|
104
|
-
|
127
|
+
```ruby
|
128
|
+
class MyAdapter < SomeOtherBuilder
|
129
|
+
include SignedForm::FormBuilder
|
105
130
|
|
106
|
-
|
131
|
+
def some_helper(field, *other_args)
|
132
|
+
add_signed_fields field
|
133
|
+
super
|
134
|
+
end
|
135
|
+
end
|
136
|
+
```
|
107
137
|
|
108
|
-
|
109
|
-
change some information about themselves though, so there's `/users/edit` as well. Now you have an admin that gets
|
110
|
-
demoted, but still has a user account. If that admin were to retain a form signature from `/admin/users/edit` they could
|
111
|
-
use that signature to modify the same fields from `/users/edit`. As a means of preventing such access SignedForm provides
|
112
|
-
the `sign_destination` option to `signed_form_for`. Example:
|
138
|
+
Then in your view:
|
113
139
|
|
114
|
-
```
|
115
|
-
<%=
|
116
|
-
<%= f.
|
117
|
-
<!-- ... -->
|
140
|
+
```erb
|
141
|
+
<%= form_for @user, signed: true, builder: MyAdapter do |f| %>
|
142
|
+
<%= f.some_helper :name %>
|
118
143
|
<% end %>
|
119
144
|
```
|
120
145
|
|
121
|
-
|
122
|
-
|
123
|
-
SignedForm
|
124
|
-
|
146
|
+
## Form Digests
|
147
|
+
|
148
|
+
SignedForm will create a digest of all the views/partials involved with rendering your form. If the form is modifed old
|
149
|
+
forms will be expired. This is done to eliminate the possibility of old forms coming back to bite you.
|
150
|
+
|
151
|
+
By default, there is a 5 minute grace period before old forms will be rejected. This is done so that if you make a
|
152
|
+
trivial change to a form you won't prevent a form a user is currently filling out from being accepted when you
|
153
|
+
restart your server.
|
154
|
+
|
155
|
+
Of course if a critical mistake is made (such as allowing an admin field to be set in the form) you could change the
|
156
|
+
secret key to prevent any old form from getting through.
|
157
|
+
|
158
|
+
By default, these digests are not cached. That means that each form that is submitted will have the views be digested
|
159
|
+
again. Most views and partials are relatively small so the cost of computing the MD5 hash of the files is not very
|
160
|
+
expensive. However, if this is something you care about SignedForm also provides a memory store
|
161
|
+
(`SignedForm::DigestStores::MemoryStore`) that will cache the digests in memory. Other stores could be used as well, as
|
162
|
+
long as the object responds to `#fetch` taking the cache key as an argument as well as the block that will return the
|
163
|
+
digest.
|
164
|
+
|
165
|
+
## Example Configuration
|
166
|
+
|
167
|
+
An example config/initializers/signed_form.rb might look something like this (these are the defaults, with the exception
|
168
|
+
of the key of course):
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
SignedForm.config do |c|
|
172
|
+
c.options[:sign_destination] = true
|
173
|
+
c.options[:digest] = true
|
174
|
+
c.options[:digest_grace_period] = 300
|
175
|
+
c.options[:signed] = false # If true, sign all forms by default
|
176
|
+
|
177
|
+
c.digest_store = SignedForm::DigestStores::NullStore.new
|
178
|
+
c.secret_key = 'supersecret'
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
Those options that are in the options hash are the default per-form options. They can be overridden by passing the same
|
183
|
+
option to the `form_for` method.
|
184
|
+
|
185
|
+
## Testing Your Controllers
|
186
|
+
|
187
|
+
Because your tests won't include a signature you will get a `ForbiddenAttributes` exception in your tests that do mass
|
188
|
+
assignment. SignedForm includes a test helper method, `permit_all_parameters` that works with both TestUnit and RSpec.
|
189
|
+
|
190
|
+
In your `spec_helper` file or `test_helper` file `require 'signed_form/test_helper'`. Then `include
|
191
|
+
SignedForm::TestHelper` in tests where you need it. An example is below.
|
192
|
+
|
193
|
+
**Caution**: `permit_all_parameters` without a block modifies the singleton class of the controller under test which
|
194
|
+
lasts for the duration of the test. If you want `permit_all_parameters` to be limited to a specific part of the test,
|
195
|
+
pass it a block and only that block will be affected. Example:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
describe CarsController do
|
199
|
+
include SignedForm::TestHelper
|
200
|
+
|
201
|
+
describe "POST create" do
|
202
|
+
it "should create a car" do
|
203
|
+
permit_all_parameters do
|
204
|
+
# This won't raise ForbiddenAttributesError
|
205
|
+
post :create, {:car => valid_attributes}, valid_session
|
206
|
+
end
|
207
|
+
|
208
|
+
# This one will raise
|
209
|
+
post :create, {:car => valid_attributes}, valid_session
|
210
|
+
|
211
|
+
# ...
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
Example without a block:
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
describe CarsController do
|
221
|
+
include SignedForm::TestHelper
|
222
|
+
|
223
|
+
describe "POST create" do
|
224
|
+
before { permit_all_parameters }
|
225
|
+
|
226
|
+
describe "with valid params" do
|
227
|
+
it "assigns a newly created car as @car" do
|
228
|
+
post :create, {:car => valid_attributes}, valid_session
|
229
|
+
|
230
|
+
assigns(:car).should be_a(Car)
|
231
|
+
assigns(:car).should be_persisted
|
232
|
+
end
|
233
|
+
|
234
|
+
# ...
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
```
|
125
239
|
|
126
|
-
|
240
|
+
## I want to hear from you
|
127
241
|
|
128
|
-
|
129
|
-
|
130
|
-
something that will be changed whenever the secret key changes.
|
242
|
+
If you're using SignedForm, I'd love to hear from you. What do you like? What could be better? I'd love to hear your
|
243
|
+
ideas. Join the mailing list on librelist to join the discussion at [signedform@librelist.com](mailto:signedform@librelist.com).
|
131
244
|
|
132
245
|
## Contributing
|
133
246
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Form is Expired</title>
|
5
|
+
<style type="text/css">
|
6
|
+
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
|
7
|
+
div.dialog {
|
8
|
+
width: 25em;
|
9
|
+
padding: 0 4em;
|
10
|
+
margin: 4em auto 0 auto;
|
11
|
+
border: 1px solid #ccc;
|
12
|
+
border-right-color: #999;
|
13
|
+
border-bottom-color: #999;
|
14
|
+
}
|
15
|
+
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
|
16
|
+
</style>
|
17
|
+
</head>
|
18
|
+
|
19
|
+
<body>
|
20
|
+
<div class="dialog">
|
21
|
+
<h1>Form Expired</h1>
|
22
|
+
<p>The form you submitted has expired. Please go back and refresh the form to try again.</p>
|
23
|
+
</div>
|
24
|
+
</body>
|
25
|
+
</html>
|
data/lib/signed_form.rb
CHANGED
@@ -5,5 +5,36 @@ require "signed_form/version"
|
|
5
5
|
require "signed_form/errors"
|
6
6
|
require "signed_form/form_builder"
|
7
7
|
require "signed_form/hmac"
|
8
|
+
require "signed_form/digest_stores"
|
9
|
+
require "signed_form/digestor"
|
8
10
|
require "signed_form/action_view/form_helper"
|
11
|
+
require "signed_form/gate_keeper"
|
9
12
|
require "signed_form/action_controller/permit_signed_params"
|
13
|
+
require "signed_form/engine" if defined?(Rails)
|
14
|
+
|
15
|
+
module SignedForm
|
16
|
+
DEFAULT_OPTIONS = {
|
17
|
+
sign_destination: true,
|
18
|
+
digest: true,
|
19
|
+
digest_grace_period: 300,
|
20
|
+
signed: false
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
class << self
|
24
|
+
attr_accessor :secret_key
|
25
|
+
|
26
|
+
attr_writer :options
|
27
|
+
def options
|
28
|
+
@options ||= DEFAULT_OPTIONS.dup
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_writer :digest_store
|
32
|
+
def digest_store
|
33
|
+
@digest_store ||= SignedForm::DigestStores::NullStore.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def config
|
37
|
+
yield self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -15,24 +15,17 @@ module SignedForm
|
|
15
15
|
def permit_signed_form_data
|
16
16
|
return if request.method == 'GET' || params['form_signature'].blank?
|
17
17
|
|
18
|
-
|
18
|
+
gate_keeper = GateKeeper.new(self)
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
allowed_attributes = Marshal.load Base64.strict_decode64(data)
|
25
|
-
options = allowed_attributes.delete(:__options__)
|
26
|
-
|
27
|
-
if options
|
28
|
-
raise Errors::InvalidURL if options[:method].to_s.casecmp(request.request_method) != 0
|
29
|
-
|
30
|
-
url = url_for(options[:url])
|
31
|
-
raise Errors::InvalidURL if url != request.fullpath && url != request.url
|
20
|
+
gate_keeper.allowed_attributes.each do |k, v|
|
21
|
+
next if params[k].nil? || v.empty?
|
22
|
+
params[k] = params[k].permit(*v)
|
32
23
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
24
|
+
rescue Errors::ExpiredForm
|
25
|
+
if defined?(Rails)
|
26
|
+
render 'signed_form/expired_form', status: 500, layout: nil
|
27
|
+
else
|
28
|
+
raise
|
36
29
|
end
|
37
30
|
end
|
38
31
|
end
|
@@ -2,13 +2,27 @@ module SignedForm
|
|
2
2
|
module ActionView
|
3
3
|
module FormHelper
|
4
4
|
|
5
|
-
# This is a wrapper around ActionView's form_for helper.
|
6
|
-
#
|
7
5
|
# @option options :sign_destination [Boolean] Only the URL given/created will be allowed to receive the form.
|
8
|
-
|
9
|
-
|
6
|
+
# @option options :digest [Boolean] Digest and verify the views have not been modified
|
7
|
+
# @option options :digest_grace_period [Integer] Time in seconds to allow old forms
|
8
|
+
# @option options :wrap_form [Symbol] Method of a form builder to wrap. Default is form_for
|
9
|
+
def form_for(record, options = {}, &block)
|
10
|
+
if options[:signed].nil? && !SignedForm.options[:signed] || !options[:signed].nil? && !options[:signed]
|
11
|
+
return super
|
12
|
+
end
|
13
|
+
|
14
|
+
options = SignedForm.options.merge options
|
15
|
+
options[:builder] ||= ::ActionView::Helpers::FormBuilder
|
16
|
+
|
17
|
+
ancestors = options[:builder].ancestors
|
18
|
+
|
19
|
+
if !ancestors.include?(::ActionView::Helpers::FormBuilder) && !ancestors.include?(SignedForm::FormBuilder)
|
20
|
+
raise "Form signing not supported on builders that don't subclass ActionView::Helpers::FormBuilder or include SignedForm::FormBuilder"
|
21
|
+
elsif !ancestors.include?(SignedForm::FormBuilder)
|
22
|
+
options[:builder] = SignedForm::FormBuilder::BUILDERS[options[:builder]]
|
23
|
+
end
|
10
24
|
|
11
|
-
|
25
|
+
super record, options do |f|
|
12
26
|
output = capture(f, &block)
|
13
27
|
f.form_signature_tag + output
|
14
28
|
end
|