-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add CSRF plugin #5727
feat: add CSRF plugin #5727
Conversation
Would you please take a look when you have time? Thanks. |
Hello there, Would you mind to pass the CI test first ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use 4 spaces to indent and remove trailing ';'
apisix/plugins/csrf.lua
Outdated
|
||
function _M.access(conf, ctx) | ||
local method = ctx.var.request_method | ||
if method == 'GET' then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why skip GET?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here a port is needed for getting the csrf token, and if all of them are blocked, the cookie may not be placed. Therefore, CORS on this port should be configured carefully, as I will explain in the documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type of request has no side effects and does not need to accept protection, but of course, I will add the corresponding instructions in the documentation.
apisix/plugins/csrf.lua
Outdated
|
||
function _M.header_filter(conf, ctx) | ||
local method = ctx.var.request_method | ||
if method == 'GET' then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why only set cookie for GET?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OPTIONS has been added to take into account cross-domain requests.
|
||
|
||
function _M.header_filter(conf, ctx) | ||
local csrf_token = gen_csrf_token(conf) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to emphasize in the doc that a new cookie is generated for each request
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This problem is not addressed?
Co-authored-by: 琚致远 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great. Let's add some tests.
You can refer to
https://github.com/apache/apisix/blob/master/docs/en/latest/internal/testing-framework.md
https://github.com/apache/apisix/blob/master/t/plugin/opa.t
no_shuffle(); | ||
no_root_location(); | ||
run_tests(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
t/plugin/csrf.t
Outdated
--- request | ||
GET /hello | ||
--- response_header | ||
Set-Cookie |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can verify part of the value with https://metacpan.org/pod/Test%3A%3ANginx%3A%3ASocket#response_headers_like
t/plugin/csrf.t
Outdated
|
||
|
||
|
||
=== TEST8: invalid csrf token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a test which token matches the header
t/plugin/csrf.t
Outdated
} | ||
--- response_body | ||
done | ||
--- no_error_log |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can do the same with --- no_error_log
too?
t/plugin/csrf.t
Outdated
|
||
|
||
|
||
=== TEST4: block request |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a space before the number
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm%(some nitpicking)
Thank you.
apisix/plugins/csrf.lua
Outdated
local ngx_encode_base64 = ngx.encode_base64 | ||
local ngx_decode_base64 = ngx.decode_base64 | ||
local ngx_time = ngx.time | ||
local cookie_time = ngx.cookie_time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For code style consistency can we use ngx_cookie_time
instead of cookie_time
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree
sha256:update(sign) | ||
local digest = sha256:final() | ||
|
||
return str.to_hex(digest) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please localize the method as from resty.string module we are using this single method.
apisix/plugins/csrf.lua
Outdated
|
||
|
||
function _M.access(conf, ctx) | ||
local safe_methods = {"GET", "HEAD", "OPTIONS"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we extract this out from the access phase and treat it as a constant array?
local SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}
docs/en/latest/plugins/csrf.md
Outdated
|
||
## Description | ||
|
||
The `CSRF` plugin based on the `Double Submit Cookie` way, protect your API from CSRF attacks. This plugin considers the `GET`, `HEAD` and `OPTIONS` methods to be safe operations. Therefore calls to the `GET`, `HEAD` and `OPTIONS` methods are not checked for interception. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it would be better to add a hyperlink at Double Submit Cookie. https://en.wikipedia.org/wiki/Cross-site_request_forgery#Double_Submit_Cookie
docs/en/latest/plugins/csrf.md
Outdated
}' | ||
``` | ||
|
||
CSRF plugin have been disabled. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CSRF plugin have been disabled. | |
The CSRF plugin has been disabled. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
apisix/plugins/csrf.lua
Outdated
local cookie = conf.name .. "=" .. csrf_token .. ";path=/;SameSite=Lax;Expires=" | ||
.. cookie_time(ngx_time() + conf.expires) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of Expires
I think we can use Max-Age
attribute here. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
Since, the operation is performed for each request, at the long run it will save some computation.
apisix/plugins/csrf.lua
Outdated
local token = { | ||
random = random, | ||
expires = conf.expires, | ||
sign = sign, | ||
} | ||
|
||
local cookie = ngx_encode_base64(core.json.encode(token)) | ||
return cookie | ||
end | ||
|
||
|
||
local function check_csrf_token(conf, ctx, token) | ||
local token_str = ngx_decode_base64(token) | ||
if token_str == nil then | ||
core.log.error("csrf token base64 decode error") | ||
return false | ||
end | ||
|
||
local token_table, err = core.json.decode(token_str) | ||
if err then | ||
core.log.error("decode token error: ", err) | ||
return false | ||
end | ||
|
||
local random = token_table["random"] | ||
if not random then | ||
core.log.error("no random in token") | ||
return false | ||
end | ||
|
||
local expires = token_table["expires"] | ||
if not expires then | ||
core.log.error("no expires in token") | ||
return false | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a proposal here, in the token object instead of total expiration seconds, can we use the expiration timestamp? So during the checking phase inside the check_csrf_token
method, we could potentially check/prevent/[minimize the window] of replay attacks as an added benefit by figuring out if the generated cookie is long been expired or not. WDYT
Thank you. cc @spacewander
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Thanks for @bisakhmondal 's suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @Baoyuantop, thanks for making the changes. Are you going to address this in this PR or do you think it would be better to have it on a separate PR? Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @bisakhmondal, I would prefer a separate PR to solve this problem, this PR is already a bit big.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Sounds good to me. Thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please open an issue for keeping track of the progress.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done @bisakhmondal. #6141
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you. Merging.
Co-authored-by: Bisakh <[email protected]>
@Baoyuantop please add this plugin to README.md too |
Done #6144 |
What this PR does / why we need it:
Add CSRF plugin for APISIX
Discuss in https://lists.apache.org/thread/ddj53svylg9s541vf2gpysxwgyrhwlzx
Pre-submission checklist: