[ANN] A Secure Cookie Protocol implementation for OpenResty

1,152 views
Skip to first unread message

Aapo Talvensaari

unread,
Jun 5, 2014, 12:37:10 PM6/5/14
to openre...@googlegroups.com
Hello Everybody,

I have created lua-resty-session module that implements Secure Cookie Protocol as described here:

Right now my session module only implements that Secure Cookie Protocol, but I'm thinking about adding server side session support with different backends to it too (i.e. file, redis, memcached). This library is a little bit similar to encrypted-session-nginx-module.

The code can be found here:

I have not yet documented the library, but it is rather straight forward to use:

location / {
    default_type text
/html;
    content_by_lua
'
        local template = require "resty.template"
        local session = require "resty.session".start()
        session.data.name = "OpenResty Fan"
        -- you need to call save if you have modified session.data table.
        -- Is there ways around it?
        session:save()
        template.render("test.html", { session = session })
    '
;
}


location
/test {
    default_type text
/html;
    content_by_lua
'
        local template = require "resty.template"
        local session = require "resty.session".start()
        template.render("test.html", { session = session })
    '
;
}


It also has a few nginx configuration directives:

set $session_name             session;
set $session_secret           623q4hR325t36VsCD3g567922IC0073T;
set $session_cookie_renew     600;
set $session_cookie_lifetime  3600;
set $session_cookie_domain    openresty.org;
set $session_cookie_secure    on;
set $session_cookie_httponly  on;
set $session_cipher_mode      cbc;
set $session_cipher_size      256;
set $session_cipher_hash      sha512;
set $session_cipher_rounds    1;

You don't need to set any values to use this. By default this module uses random secret that is available until nginx is restarted. If you have a server farm, you need to set at least the $session_cookie_secure. Other config variables also have reasonable, and safe defaults. Config parameters can also be overridden with e.g. require "resty.session".start({ name = "MyApplication", cookie = { lifetime = 600, renew = 160 }})

The library dependencies all come with default OpenResty bundle, and it is pure Lua library.

What I'm looking here are the comments about the implementation. All feedback is welcomed. One thing that I'm a little bit concerned is ngx.header["Set-Cookie"] = ... call. What is the correct way to handle this situation if client code also wants to set different cookies (nginx overrides previous headers)? Of course you may not need other cookies anyway as you can store them in session.data.cart = { items = { 1, 10, 200} }.

I will document the library as soon as I have time for that. Probably in the next few weeks.

Regards
Aapo



Aapo Talvensaari

unread,
Jun 5, 2014, 1:04:03 PM6/5/14
to openre...@googlegroups.com
If you have a server farm, you need to set at least the $session_cookie_secure.

$session_secret was what I meant to say of course, ;-). 

Aapo Talvensaari

unread,
Jun 6, 2014, 6:16:30 AM6/6/14
to openre...@googlegroups.com
On Thursday, June 5, 2014 7:37:10 PM UTC+3, Aapo Talvensaari wrote:
location / {
    default_type text
/html;
    content_by_lua
'

        local session = require "resty.session".start()
    ';
}

On thing I was also thinking it this "require "resty.session".start()" that would it make more sense to just have user enter this when he/she needs sessions:

require "resty.session"

That defines global session variable. The session is automatically started with this require so you could then just:

local session = session
session
.data.name = "OpenResty Fan"
session
:save()

This way the API would be a little bit cleaner and there is no problem if "start" called many times (right now if start it called many time, it will overwrite current session with {} as data or if session was already set with the data that came from the user supplied session cookie, and it was checked to be valid, its data would be used (overwriting previously started session and data).

I have also added a few more config vars (default values are as follows):

set $session_check_ua          on;
set $session_check_scheme      on;
set $session_check_addr        off;
set $session_identifier_length 16;

I know that global variables are kinda "bad", but for session this might make sense. But are these globals global for every request or is it global to only the request that made it?


Regards
Aapo

Aapo Talvensaari

unread,
Jun 6, 2014, 9:26:05 AM6/6/14
to openre...@googlegroups.com
Hi all,

I have fixed some bugs, and started documenting this library. The lastest documentation can be found from here:

Please let me know if you have any questions or suggestions!


Regards
Aapo

Yichun Zhang (agentzh)

unread,
Jun 6, 2014, 3:21:24 PM6/6/14
to openresty-en
Hello!

On Fri, Jun 6, 2014 at 3:16 AM, Aapo Talvensaari wrote:
> require "resty.session"
>
> That defines global session variable. The session is automatically started
> with this require so you could then just:
>
> local session = session
> session.data.name = "OpenResty Fan"
> session:save()
>

Oh, it is a really bad idea to use Lua globals, especially not as a
module API. It is always bad to pollute the global namespace for
whatever reasons. It also makes the tests of the lua-releng tool fail
to pass for the user Lua code.

The user herself can always save the per-request session into the
ngx.ctx table anyway, which survives longer than Lua globals because
Lua globals cannot go across the boundary of the current request
handler while a request can have multiple request handlers (like
content_by_lua and log_by_lua). And even require()'ing everytime she
needs the session is not really bad because require() always looks up
the loaded module data in the package.loaded table (hooked in the Lua
registry) and the fast code path can even be fully JIT compiled when
lua-resty-core is used.

Just my 2 cents :)

Best regards,
-agentzh

Aapo Talvensaari

unread,
Jun 6, 2014, 4:15:51 PM6/6/14
to openre...@googlegroups.com
On Friday, June 6, 2014 10:21:24 PM UTC+3, agentzh wrote:
Oh, it is a really bad idea to use Lua globals, especially not as a
module API. 
The user herself can always save the per-request session into the
ngx.ctx table anyway, which survives longer than Lua globals because
Lua globals cannot go across the boundary of the current request
handler while a request can have multiple request handlers (like
content_by_lua and log_by_lua). And even require()'ing everytime she
needs the session is not really bad because require() always looks up
the loaded module data in the package.loaded table (hooked in the Lua
registry)

That's what I thought too.

So would it be okay to advice users of my lua-resty-session module to do this:

local session = ngx.ctx.session = ngx.ctx.session or require "resty.session".start()

What I mean is that:

1. I need to load module require "resty.session"
2. Start session only once, and if user tries to start it many times in user lua scripts or modules, always return the same Lua table that is bound to current request (on single request I need to keep exactly one single session, that is not require "resty.session" but require "resty.session".start().

Or should my module automatically reqister ngx.ctx.session variable? Or should I just advice users that if require "resty.session".start() is called many times, each call will overwrite the previous session, so that the user should himself/herself manage the session variable himself. The thing here is that sessions are so in the core of web programming (ie. in php you just call session_start (or in case of autostarted sessions you don't need even that) and then use global request bound $_SESSION, and that's it). You don't need even call session:save as PHP will automatically call session saving functionn.

Of course, the PHP's way is not the only or even best way to archieve this. I'm just looking a way that would be most natural in OpenResty. The current API of my lua-resty-session doesn't look too bad in that regard, but it has these tiny problems. Say that user has Lua module A and B and both are utilized in every request. It would be nice just to say local session = require "resty.session" in each of these modules. But as you can see, this just returns the same table always and that cannot be used as each request need dynamically generated session everytime. That's why the current api is rather like this local session = require"resty.session".start(). But the problem here is that you cannot do this multiple times during the same request (as each call overwrites others). So I need to store previously generated session somewhere, like ngx.ctx (assuming you can store Lua tables in ngx.ctx?). Then I would be polluting ngx.ctx table. That may be reasonable when documented. The other thing is that there aren't hooks in OpenResty (or is there) where I could attach session:save and set ngx.header (before the body is send).

Just talking aloud.... Yichun, if you can lead me to right path, that would certainly help.


Regards
Aapo

Yichun Zhang (agentzh)

unread,
Jun 6, 2014, 4:40:04 PM6/6/14
to openresty-en
Hello!

On Fri, Jun 6, 2014 at 1:15 PM, Aapo Talvensaari wrote:
> So would it be okay to advice users of my lua-resty-session module to do
> this:
>
> local session = ngx.ctx.session = ngx.ctx.session or require
> "resty.session".start()
>

This line is a bit too long and rather confusing :)

> What I mean is that:
>
> 1. I need to load module require "resty.session"
> 2. Start session only once, and if user tries to start it many times in user
> lua scripts or modules, always return the same Lua table that is bound to
> current request (on single request I need to keep exactly one single
> session, that is not require "resty.session" but require
> "resty.session".start().
>

Personally I'd just let the user do the old school way. Too much magic
may defeat clever user's optimization efforts ;) For example, fetching
ngx.ctx from scratch is also expensive (even more expensive than
fetching a Lua global).

> Or should my module automatically reqister ngx.ctx.session variable?

It is generally not a good idea to pollute the key space of ngx.ctx
with your libraries' private data :) There is a risk of collisions
with other things. But you could do that if you explicitly state the
key you'd insert in the protocol (i.e., in docs).

As I've said, fetching ngx.ctx from scratch is expensive (the
metamethod call involved in ngx["ctx"] is expensive). The user may
choose to pass the resulting ctx table around by explicit function
arguments around her own Lua code base. So better leave the choice to
your user :)

> Or
> should I just advice users that if require "resty.session".start() is called
> many times, each call will overwrite the previous session, so that the user
> should himself/herself manage the session variable himself.

Yes, this is the recommended approach. Be simple and be explicit :)

> The thing here
> is that sessions are so in the core of web programming (ie. in php you just
> call session_start (or in case of autostarted sessions you don't need even
> that) and then use global request bound $_SESSION, and that's it). You don't
> need even call session:save as PHP will automatically call session saving
> functionn.

I know. But IMHO usually there are more per-request data in addition
to the session token itself. Better give the user the freedom to
manage per-request data in her own favorite way :)

>
> Of course, the PHP's way is not the only or even best way to archieve this.
> I'm just looking a way that would be most natural in OpenResty. The current
> API of my lua-resty-session doesn't look too bad in that regard, but it has
> these tiny problems. Say that user has Lua module A and B and both are
> utilized in every request. It would be nice just to say local session =
> require "resty.session" in each of these modules. But as you can see, this
> just returns the same table always and that cannot be used as each request
> need dynamically generated session everytime.

Try always passing the per-request data (including sessions)
explicitly via these Lua modules' method arguments or constructor
arguments. This is the recommended paradigm. The data scoping issue
can be really annoying and we'd better be more explicit here. Plus,
this also helps performance a lot :) For example, I recently avoided
the scattered ngx.ctx calls across our Lua CDN code base by passing
the resulting ngx.ctx table around explicitly by function call
arguments, which gives almost 15% overall speedup in my local
benchmark.

Magic always comes at a price and I've been regretting adding too much
magic to existing ngx_lua's Lua API, which makes future optimizations
and tuning harder than desired. I'll try to address these by adding
new better alternatives and deprecating existing bad ones :) And yeah,
I'll try hard not to break backward compatibility :)

Again, there are just my personal opinions :)

Best regards,
-agentzh

bell...@gmail.com

unread,
Jul 17, 2023, 10:03:30 PM7/17/23
to openresty-en
This is an old thread, and I came across it while researching a few considerations I have around Secure Sessions in my project.

It's too early in the project to go into too many details, but one thing I have noticed while working for a large Enterprise Client is that there are a group of scripters exploiting Secure Session ID's. Their aim is not exactly hackish- they're not really trying to hijack secure sessions. What they are trying to do is to reuse Session ID's (not the tokens, but the ID's themselves) and distribute these across several IP's to beat Rate Limits. 

The reason I mention this here is not to debate how to harden Sessions themselves- I think this thread is already well resolved thanks to the explanation about the "Openresty paradigm" given by Agent Z - 

I just wanted to add some input and color to the larger conversation of abuse that Secure Sessions themselves address- what about Scripting and the insidious escalations in traffic that seem to occur from Sep-Jan on most worldwide sites of import?

So I have reviewed using obscure "Session" cookies that are not well enforced - and the rationale behind this is twofold:
1. Performance (as in adding as little overhead as possible) 
2. Making abuse transparent. IF someone is exploiting these ID's beyond an acceptable threshold, then they are flagged and thrown into a completely different Rate Limiting Pool. 

In this way, there is a clean and efficient model to ID abuse, which works in parallel with the Secure Sessions. Doing this is valuable in itself, rather than over-engineer a solution to prevent the abuse. It just so happens there is a massively effective solution once the flags are set.

The point here is less what to do with Secure Sessions, and more what NOT to do with them. Let us not overload other logic on top of the role they serve, even if such things seem to accomplish more "magic" ... sometimes less is more.
Reply all
Reply to author
Forward
0 new messages