signed_form 0.1.2 → 0.2.0
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.
- 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
|
[](http://badge.fury.io/rb/signed_form)
|
4
4
|
[](https://travis-ci.org/erichmenge/signed_form)
|
5
5
|
[](https://codeclimate.com/github/erichmenge/signed_form)
|
6
|
+
[](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
|