Skip to content
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

Merged
merged 29 commits into from
Jan 18, 2022
Merged

feat: add CSRF plugin #5727

merged 29 commits into from
Jan 18, 2022

Conversation

Baoyuantop
Copy link
Contributor

@Baoyuantop Baoyuantop commented Dec 7, 2021

What this PR does / why we need it:

Add CSRF plugin for APISIX
Discuss in https://lists.apache.org/thread/ddj53svylg9s541vf2gpysxwgyrhwlzx

Pre-submission checklist:

  • Did you explain what problem does this PR solve? Or what new features have been added?
  • Have you added corresponding test cases?
  • Have you modified the corresponding document?
  • Is this PR backward compatible? If it is not backward compatible, please discuss on the mailing list first

apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
@Baoyuantop Baoyuantop marked this pull request as ready for review December 14, 2021 13:56
@Baoyuantop
Copy link
Contributor Author

Would you please take a look when you have time? Thanks.

@leslie-tsang
Copy link
Member

Would you please take a look when you have time? Thanks.

Hello there, Would you mind to pass the CI test first ?

Copy link
Member

@spacewander spacewander left a 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 Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved

function _M.access(conf, ctx)
local method = ctx.var.request_method
if method == 'GET' then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why skip GET?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.


function _M.header_filter(conf, ctx)
local method = ctx.var.request_method
if method == 'GET' then
Copy link
Member

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?

Copy link
Contributor Author

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.

apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
conf/config-default.yaml Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Show resolved Hide resolved
apisix/plugins/csrf.lua Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved
docs/en/latest/plugins/csrf.md Outdated Show resolved Hide resolved
apisix/plugins/csrf.lua Outdated Show resolved Hide resolved


function _M.header_filter(conf, ctx)
local csrf_token = gen_csrf_token(conf)
Copy link
Member

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

Copy link
Member

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?

Copy link
Member

@spacewander spacewander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no_shuffle();
no_root_location();
run_tests();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add

add_block_preprocessor(sub {
and remove duplicated --- request.

t/plugin/csrf.t Outdated
--- request
GET /hello
--- response_header
Set-Cookie
Copy link
Member

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



=== TEST8: invalid csrf token
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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

Copy link
Member

@bisakhmondal bisakhmondal left a 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.

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
Copy link
Member

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?

Copy link
Contributor Author

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)
Copy link
Member

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.



function _M.access(conf, ctx)
local safe_methods = {"GET", "HEAD", "OPTIONS"}
Copy link
Member

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"}


## 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.
Copy link
Member

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

}'
```

CSRF plugin have been disabled.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CSRF plugin have been disabled.
The CSRF plugin has been disabled.

Copy link
Member

@juzhiyuan juzhiyuan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines 155 to 156
local cookie = conf.name .. "=" .. csrf_token .. ";path=/;SameSite=Lax;Expires="
.. cookie_time(ngx_time() + conf.expires)
Copy link
Member

@bisakhmondal bisakhmondal Jan 16, 2022

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.

Comment on lines 79 to 113
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
Copy link
Member

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

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Merging.

@bisakhmondal bisakhmondal merged commit 4d52c3e into apache:master Jan 18, 2022
@Baoyuantop Baoyuantop deleted the feat-csrf branch January 18, 2022 08:26
@moonming
Copy link
Member

@Baoyuantop please add this plugin to README.md too

@Baoyuantop
Copy link
Contributor Author

@Baoyuantop please add this plugin to README.md too

Done #6144

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants