tide icon indicating copy to clipboard operation
tide copied to clipboard

regenerating session don't send a new cookie

Open Randati opened this issue 5 years ago • 7 comments

SessionMiddleware seems to ignore session regeneration. The middleware should reset the session cookie with a new id after session.regenerate(), but instead it continues to use the old cookie. This can be a security vulnerability because it allows session fixation attacks.

Here is a simple test case. The session id should change after logging in, but it doesn't.

use tide::sessions::{MemoryStore, SessionMiddleware};
use tide::{Redirect, Request, Response};

#[async_std::main]
async fn main() -> tide::Result<()> {
    let mut app = tide::new();

    app.with(SessionMiddleware::new(
        MemoryStore::new(),
        b"01234567890123456789012345678901",
    ));

    app.at("/").get(index);
    app.at("/login").get(login);

    app.listen("127.0.0.1:8081").await?;
    Ok(())
}

async fn index(req: Request<()>) -> tide::Result {
    let session = req.session();
    let user: String = session.get("user").unwrap_or("guest".to_string());

    let mut resp: Response = format!(
        r#"
        Logged in as {}<br>
        Session id: {}<br>
        <a href="/login">Login</a>"#,
        user,
        session.id()
    )
    .into();

    resp.set_content_type("text/html");
    Ok(resp)
}

async fn login(mut req: Request<()>) -> tide::Result {
    let session = req.session_mut();
    session.regenerate();
    session.insert("user", "admin")?;
    Ok(Redirect::see_other("/").into())
}

Randati avatar Dec 11 '20 15:12 Randati

Hi @Randati, the session is regenerated but tide don't send the new cookie on Redirect ( ping @yoshuawuyts, I think is ok to not doit but just to confirm ). If you send a Response you would see the change in the session id.

You can see the headers of the actual example code

req:

GET /login HTTP/1.1
Host: localhost:8081
Cookie: tide.sid=U%2Fq2s7AVUz8iigWrrJTK+iGTfDDzXn3nKhTeaBQ56BQ%3DQYJQNb7vi0+OuavD13EbjynrnFdlCDJbyuZyyX+Cbl3wnCUlJMZ+io+Q8O4uPaucyyyH5jSJ5gd96HrZ1ATxpQ%3D%3D

res:

HTTP/1.1 303 See Other
content-length: 0
date: Thu, 17 Dec 2020 14:42:43 GMT
location: /

pepoviola avatar Dec 17 '20 15:12 pepoviola

@pepoviola odd, thanks for digging deeper into this. I would have expected the redirect still to have the right headers applied to it. This indeed registers as a bug, and we should make sure to have the right headers applied to it.

yoshuawuyts avatar Dec 18 '20 14:12 yoshuawuyts

Hi @yoshuawuyts , thanks for the reply and clarification. I can check why we don't send the right headers. Thx!

pepoviola avatar Dec 18 '20 14:12 pepoviola

Hi @yoshuawuyts, I take another look of this issue and the problem here is that we are regenerating the session but not the cookie id ( as @Randati mention ). So the current session is changed but when the pages reload is back to the original one since the cookie id isn't changed.

I tracked the code and look like we are cloning the session to set the request ext

        request.set_ext(session.clone());

        let mut response = next.run(request).await;

        if session.is_destroyed() { (...)

But the we continue to use the same session and if the endpoint ( like in this test case ) change the session we don't know about that in the middleware. I think we need some kind of arc/mutex to allow mutate the session by the endpoint ( or other middleware ) but I'm not sure how is the best approach to implement that.

Related lines: https://github.com/http-rs/tide/blob/main/src/sessions/middleware.rs#L95-L97

Trace:

  • first request ( session created and set cookie )
[2020-12-18T22:35:13Z INFO  tide::log::middleware] <-- Request received
[2020-12-18T22:35:13Z TRACE async_session::memory_store] storing session by id `TCcNZEfNFVUVy8Jo3P0pUju0EHJC2z9F2Q2xL7J7/EU=`
[2020-12-18T22:35:13Z INFO  tide::log::middleware] --> Response sent
  • second request ( with session.regenerate call in endpoint )
[2020-12-18T22:35:20Z INFO  tide::log::middleware] <-- Request received
[2020-12-18T22:35:20Z TRACE async_session::memory_store] loading session by id `TCcNZEfNFVUVy8Jo3P0pUju0EHJC2z9F2Q2xL7J7/EU=`
[2020-12-18T22:35:20Z TRACE async_session::session] regenerate call for session id `TCcNZEfNFVUVy8Jo3P0pUju0EHJC2z9F2Q2xL7J7/EU=`
[2020-12-18T22:35:20Z TRACE async_session::session] new session id `+mLrLlbMa6PRKl0uCpY4iCvh4VV59qa04uH0cjBU5kQ=`
[2020-12-18T22:35:20Z TRACE async_session::memory_store] storing session by id `TCcNZEfNFVUVy8Jo3P0pUju0EHJC2z9F2Q2xL7J7/EU=`
[2020-12-18T22:35:20Z INFO  tide::log::middleware] --> Response sent

Thx!

pepoviola avatar Dec 18 '20 22:12 pepoviola

Here's a hacky temporary workaround which seems to do the job (replace the location with where you need to redirect):

    req.session_mut().regenerate();
    req.session_mut().insert("user", "admin")?;

    let mut resp: tide::Response = r#"
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="UTF-8" />
          <meta http-equiv="X-UA-Compatible" content="IE=edge" />
          <meta name="viewport" content="width=device-width, initial-scale=1.0" />
          <title>Document</title>
          <script>window.location.href="/"</script>
        </head>
      </html>        
      "#
    .into();

    resp.set_content_type(mime::HTML);

    Ok(resp)

jlkiri avatar Feb 15 '21 07:02 jlkiri

Hi @jlkiri, thanks for the workaround. I just changed the issue title since the bug is that we are not sending a new cookie when we call session.regenerate. The fix for this is still in progress.

Thx!

pepoviola avatar Feb 15 '21 11:02 pepoviola

I'm facing same issue, so implemeted some workaround like below. It seems to work as intended.

/// A workaround for tide issue #762.
/// https://github.com/http-rs/tide/issues/762
pub trait SessionWorkaroundExt {
    /// Session key of regeneration flag.
    const REGENERATION_MARK_KEY: &'static str;

    /// Marks the session for ID regeneration.
    fn mark_for_regenerate(&mut self);

    /// Checks whether the session should regenerate the ID.
    /// The session key `REGENERATION_MARK` will be removved.
    fn should_regenerate(&mut self) -> bool;
}

impl SessionWorkaroundExt for Session {
    const REGENERATION_MARK_KEY: &'static str = "sid-regenerate";

    fn mark_for_regenerate(&mut self) {
        self.insert(Self::REGENERATION_MARK_KEY, true).expect("Boolean should be serialized");
    }

    fn should_regenerate(&mut self) -> bool {
        let previously_changed = self.data_changed();
        let regenerate = self.get(Self::REGENERATION_MARK_KEY).unwrap_or_default();

        self.remove(Self::REGENERATION_MARK_KEY);
        if !previously_changed {
            self.reset_data_changed();
        }

        regenerate
    }
}

In SessionStore::store_session():

if session.should_regenerate() {
    session.regenerate();
}

In endpoints:

request.session_mut().mark_for_regenerate();

kb10uy avatar Mar 28 '21 01:03 kb10uy