Skip to content

Commit

Permalink
Bump version to 3.79.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyevans committed Apr 12, 2024
1 parent 0a2e9fc commit 446633c
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 2 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
= master
= 3.79.0 (2024-04-12)

* Do not update template mtime when there is an error reloading templates in the render plugin (jeremyevans)

Expand Down
148 changes: 148 additions & 0 deletions doc/release_notes/3.79.0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
= New Features

* The hmac_paths plugin allows protection of paths using an HMAC. This can be used
to prevent users enumerating paths, since only paths with valid HMACs will be
respected.

To use the plugin, you must provide a :secret option. This sets the secret for
the HMACs. Make sure to keep this value secret, as this plugin does not provide
protection against users who know the secret value. The secret must be at least
32 bytes.

plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes'

To generate a valid HMAC path, you call the hmac_path method:

hmac_path('/widget/1')
# => "/0c2feaefdfc80cc73da19b060c713d4193c57022815238c6657ce2d99b5925eb/0/widget/1"

The first segment in the returned path is the HMAC. The second segment is flags for
the type of paths (see below), and the rest of the path is as given.

To protect a path or any subsection in the routing tree, you wrap the related code
in an +r.hmac_path+ block.

route do |r|
r.hmac_path do
r.get 'widget', Integer do |widget_id|
# ...
end
end
end

If first segment of the remaining path contains a valid HMAC for the rest of the path (considering
the flags), then r.hmac_path will match and yield to the block, and routing continues inside
the block with the HMAC and flags segments removed.

In the above example, if you provide a user a link for widget with ID 1, there is no way
for them to guess the valid path for the widget with ID 2, preventing a user from
enumerating widgets, without relying on custom access control. Users can only access
paths that have been generated by the application and provided to them, either directly
or indirectly.

In the above example, r.hmac_path is used at the root of the routing tree. If you
would like to call it below the root of the routing tree, it works correctly, but you
must pass hmac_path the :root option specifying where r.hmac_paths will be called from.
Consider this example:

route do |r|
r.on 'widget' do
r.hmac_path do
r.get Integer do |widget_id|
# ...
end
end
end

r.on 'foobar' do
r.hmac_path do
r.get Integer do |foobar_id|
# ...
end
end
end
end

For security reasons, the hmac_path plugin does not allow an HMAC path designed for
widgets to be a valid match in the r.hmac_path call inside the "r.on 'foobar'"
block, preventing users who have a valid HMAC for a widget from looking at the page for
a foobar with the same ID. When generating HMAC paths where the matching r.hmac_path
call is not at the root of the routing tree, you must pass the :root option:

hmac_path('/1', root: '/widget')
# => "/widget/daccafce3ce0df52e5ce774626779eaa7286085fcbde1e4681c74175ff0bbacd/0/1"

hmac_path('/1', root: '/foobar')
# => "/foobar/c5fdaf482771d4f9f38cc13a1b2832929026a4ceb05e98ed6a0cd5a00bf180b7/0/1"

Note how the HMAC changes even though the path is the same.

In addition to the +:root+ option, there are additional options that further constrain
use of the generated paths.

The :method option creates a path that can only be called with a certain request
method:

hmac_path('/widget/1', method: :get)
# => "/d38c1e634ecf9a3c0ab9d0832555b035d91b35069efcbf2670b0dfefd4b62fdd/m/widget/1"

Note how this results in a different HMAC than the original hmac_path('/widget/1')
call. This sets the flags segment to "m", which means r.hmac_path will consider the
request mehod when checking the HMAC, and will only match if the provided request method
is GET. This allows you to provide a user the ability to submit a GET request for the
underlying path, without providing them the ability to submit a POST request for the
underlying path, with no other access control.

The :params option accepts a hash of params, converts it into a query string, and
includes the query string in the returned path. It sets the flags segment to +p+, which
means r.hmac_path will check for that exact query string. Requests with an empty query
string or a different string will not match.

hmac_path('/widget/1', params: {foo: 'bar'})
# => "/fe8d03f9572d5af6c2866295bd3c12c2ea11d290b1cbd016c3b68ee36a678139/p/widget/1?foo=bar"

For GET requests, which cannot have request bodies, that is sufficient to ensure that the
submitted params are exactly as specified. However, POST requests can have request bodies,
and request body params override query string params in r.params. So if you are using
this for POST requests (or other HTTP verbs that can have request bodies), use r.GET
instead of r.params to specifically check query string parameters.

You can use +:root+, +:method+, and +:params+ at the same time:

hmac_path('/1', root: '/widget', method: :get, params: {foo: 'bar'})
# => "/widget/9169af1b8f40c62a1c2bb15b1b377c65bda681b8efded0e613a4176387468c15/mp/1?foo=bar"

This gives you a path only valid for a GET request with a root of "/widget" and
a query string of "foo=bar".

To handle secret rotation, you can provide an :old_secret option when loading the
plugin.

plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
old_secret: 'previous-secret-value-with-at-least-32-bytes'

This will use :secret for constructing new paths, but will respect paths generated by
:old_secret.

= Other Improvements

* When not using cached templates in the render plugin, the render plugin
now has better handling when a template is modified and results in an
error. Previously, the error would be raised on the first request after
the template modification, but subsequent requests would use the
previous template value. The render plugin will no longer update the
last modified time in this case, so if a template is modified and
introduces an error (e.g. SyntaxError in an erb template), all future
requests that use the template will result in the error being raised,
until the template is fixed.

= Backwards Compatibility

* The internal TemplateMtimeWrapper API has been modified. As documented,
this is an internal class and the API can change in any Roda version.
However, if any code was relying on the previous implementation of
TemplateMtimeWrapper#modified?, it will need to be modified, as that
method has been replaced with TemplateMtimeWrapper#if_modified.

Additionally, the TemplateMtimeWrapper#compiled_method_lambda API has
also changed.
2 changes: 1 addition & 1 deletion lib/roda/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Roda
RodaMajorVersion = 3

# The minor version of Roda, updated for new feature releases of Roda.
RodaMinorVersion = 78
RodaMinorVersion = 79

# The patch version of Roda, updated only for bug fixes from the last
# feature release.
Expand Down

0 comments on commit 446633c

Please sign in to comment.