Badgerati / Pode

Pode is a Cross-Platform PowerShell web framework for creating REST APIs, Web Sites, and TCP/SMTP servers
https://badgerati.github.io/Pode
MIT License
852 stars 93 forks source link

CSRF Example failing #810

Closed phatmandrake closed 3 years ago

phatmandrake commented 3 years ago

Question

Attempting to use the example at the bottom of https://badgerati.github.io/Pode/Tutorials/Middleware/Types/CSRF/

The view generates the webpage, but when pressing submit Pode says the CSRF token is invalid.

Badgerati commented 3 years ago

The Enable-PodeSessionMiddleware is missing a -Duration :) I did fix all of these at one point, must have slipped through the cracks!

Set the session to the following, and it should be good:

Enable-PodeSessionMiddleware -Duration 120
phatmandrake commented 3 years ago

Okay it refreshes now without error, but I'm not seeing the flash message 🤔

Badgerati commented 3 years ago

Huh, I just ran the example (with the above change) and get the flash message back 🤔

image

phatmandrake commented 3 years ago

How does this look 🤔

Start-PodeServer -Browse {
    Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http
    Set-PodeViewEngine -Type Pode

    # setup session and csrf middleware
    Enable-PodeSessionMiddleware -Secret 'vegeta' -Duration 120
    Enable-PodeCsrfMiddleware

    # this route will work, as GET methods are ignored by CSRF by default
    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        $token = (New-PodeCsrfToken)
        Write-PodeViewResponse -Path 'index' -Data @{ 'csrfToken' = $token } -FlashMessages
    }

    # POST route for form which will require the csrf token from above
    Add-PodeRoute -Method Post -Path '/token' -ScriptBlock {
        Add-PodeFlashMessage -Name 'message' -Message $WebEvent.Data['message']
        Move-PodeResponseUrl -Url '/'
    }
}
<html>
    <head>
        <title>CSRF Example Page</title>
    </head>
    <body>
        <h1>Example form using a CSRF token</h1>
        <p>Clicking submit will just reload the page with your message</p>

        <form action='/token' method='POST'>
            <!-- the hidden input for the CSRF token needs to have the name 'pode.csrf' -->
            <input type='hidden' name='pode.csrf' value='$($data.csrfToken)' />
            <input type='text' name='message' placeholder='Enter any random text' />
            <input type='submit' value='Submit' />
        </form>

        <!-- on the page reload, display your message -->
        $(if ($data.flash['message']) {
            "<p>$($data.flash['message'])</p>"
        })
    </body>
</html>
Badgerati commented 3 years ago

Yup, works fine 👀

image

phatmandrake commented 3 years ago

Interesting. Test-PodeFlashMessage doesn't seem to recognize the the message exist if I test for it at the default path before the view response. It does after it adds the flash message in the Token route.

Does this imply it may not be tracking the session?

Badgerati commented 3 years ago

I did this:

Start-PodeServer -Browse {
    Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http
    Set-PodeViewEngine -Type Pode

    # setup session and csrf middleware
    Enable-PodeSessionMiddleware -Secret 'vegeta' -Duration 120
    Enable-PodeCsrfMiddleware

    # this route will work, as GET methods are ignored by CSRF by default
    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        $token = (New-PodeCsrfToken)
        Test-PodeFlashMessage -Name 'message' | Out-Default
        Write-PodeViewResponse -Path 'index' -Data @{ 'csrfToken' = $token } -FlashMessages
    }

    # POST route for form which will require the csrf token from above
    Add-PodeRoute -Method Post -Path '/token' -ScriptBlock {
        Add-PodeFlashMessage -Name 'message' -Message $WebEvent.Data['message']
        Test-PodeFlashMessage -Name 'message' | Out-Default
        Move-PodeResponseUrl -Url '/'
    }
}

I get False on the initial page load, but thereafter I get True from the /token route and the / route 🤔

Is the code above similar to what you have with your Test-PodeFlashMessage or different?

phatmandrake commented 3 years ago

I'm getting False-True-False. It doesn't think there's a flash message after the redirect.

Badgerati commented 3 years ago

That is strange 🤔 which version of Pode/PowerShell are you using? Because you're right, that does sound like something's preventing it from saving the session's data.

All this is running locally your your machine, I assume?

phatmandrake commented 3 years ago

Local, MacOS, 7.1.4, 2.4.1 All up to date.

Badgerati commented 3 years ago

I just tried those on Windows and Ubuntu, both worked as expected 🤔 Are you able to try either of those and see what happens?

Do you get anything by adding New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging as well? Just in case.

phatmandrake commented 3 years ago

Man I am really striking out today.

CentOS 8: (Currently using for REST endpoints). Started another instance of pode, for some reason Pode is unable to find index.pode and returns 404 --- the file is for sure in the current directory that pode is being invoked from. No idea what that's about ....

Windows: Still No flash message. First time install for Pode there as well.

phatmandrake commented 3 years ago

When I ran the server as root (CentOS 8) these errors popped up

image

Badgerati commented 3 years ago

This is bizarre! I just tried on a fresh Windows VM and it worked 🤯

The errors from CentOS are probably because there's no browser to be opened - removing -Browser should stop those.

I guess to be absolutely sure I'm doing everything the same: if you're using the just the example would just be able to attach a zip/7z of the server? If not, could you show a screenshot of the file/folder structure; an example of the code (which I'm guessing is just the example! 😂); and the steps you're taking on the page/form itself?

phatmandrake commented 3 years ago

It's just a single folder and the example. I'm gonna be embarrassed if I forgot a comma somewhere. 😆

Flash_Message_Test.zip

Badgerati commented 3 years ago

First thing I spotted is the index.pode should be in a views folder :) I've updated the docs to fix that it doesn't mention that!

image

After that, it works for me (on Windows it worked even without the views folder, which I probably need to look into!) 🤔

phatmandrake commented 3 years ago

I'm gonna find another laptop to try this on https://user-images.githubusercontent.com/24230425/130709543-5a214786-8434-4909-8765-6e8397b5edf5.mov

Badgerati commented 3 years ago

What happens if you take out the CSRF?

Commenting out:

Start-PodeServer  {
    Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http
    Set-PodeViewEngine -Type Pode

    # setup session and csrf middleware
    Enable-PodeSessionMiddleware -Secret 'vegeta' -Duration 120
    #Enable-PodeCsrfMiddleware

    # this route will work, as GET methods are ignored by CSRF by default
    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        #$token = (New-PodeCsrfToken)
        Test-PodeFlashMessage -Name 'message' | Out-Default
        Write-PodeViewResponse -Path 'index' -Data @{ 'csrfToken' = $token } -FlashMessages
    }

    # POST route for form which will require the csrf token from above
    Add-PodeRoute -Method Post -Path '/token' -ScriptBlock {
        Add-PodeFlashMessage -Name 'message' -Message $WebEvent.Data['message']
        Test-PodeFlashMessage -Name 'message' | Out-Default
        Move-PodeResponseUrl -Url '/'
    }
}

Does that still exhibit the same problem?

I just tried a different timezone, in case the cookie was being weird, but is you local set to UTC or something else? I'm wondering if the session is being immediately timed out somehow, even though it has a duration of 2mins. If you open dev-tools in the browser, and go to Application > Cookies. If the pode.sid cookie value changes everytime you click "Submit" in the form, the session is being reset each time.

phatmandrake commented 3 years ago

Well the message shows when I comment those lines out. I am using a standard time convention, and the session id is definitely changing each time. How is this value supposed to be stored?

Badgerati commented 3 years ago

I managed to get a couple other people to try the example, and for them it worked - one of them was also on MacOS as well. Someone suggested what happens if you disable the browser extensions, or try in a different browser?

If commenting out the CSRF lines makes this work, then it suggests that sessions are working ok and it's the CSRF part at fault 🤔 The flash value is written to the current session and saved to Pode's in-memory storage. When a session has data saved, then the pode.sid cookie is updated with the expiry so it lasts for 2mins. The session middleware gets the cookie, and gets the session data from the in-memory store, and reads the flash message from there. If the session with CSRF keeps changing, then somehow it's stopping it from saving the session properly.

(The in-memory store for session is simply a PSObject and Hashtable).

phatmandrake commented 3 years ago

I have tried on 5 computers with the same results a Windows 10 VM, A Centos 8 VM, A Win Server 2016 VM, MacOS Catalina, my Windows 10 Gaming Desktop. All the same issue.

phatmandrake commented 3 years ago

And I finally got ahold of another Mac - fresh build - Big Sur - Pode 1.4.1 - PS 7.1.4 - still not working. Edit: It works if I use the -UseCookies option on Enable-PodeCsrfMiddleware

Maybe you can give me a zip file of you working one.

However:

https://badgerati.github.io/Pode/Tutorials/Middleware/Types/Sessions/

The example at the bottom does not work as is. The server fails to start since no endpoint is specified.

Start-PodeServer {
    Enable-PodeSessionMiddleware -Duration 120

    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        $WebEvent.Session.Data.Views++
        Write-PodeJsonResponse -Value @{ 'Views' = $WebEvent.Session.Data.Views }
    }
}

Adjusting it. I can refresh the page once and it will increment to 2, but then it resets back to 1 on subsequent resets. Shouldn't it increment until the session duration elapses? Also Get-PodeSessionId | Out-Default doesn't render anything in the following example.

Start-PodeServer {
    Enable-PodeSessionMiddleware -Duration 120

    Add-PodeEndpoint -Address localhost -Protocol http -Port 8081

    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        Get-PodeSessionId | Out-Default
        $WebEvent.Session.Data.Views++
        Write-PodeJsonResponse -Value @{ 'Views' = $WebEvent.Session.Data.Views }
    }
}

It definitely seems like sessions just aren't working. I don't fully understand how sessions work. If sessions are stored server side how does Pode recognize the browser as being the same without using a cookie, or passing it along in the header.

Edit: It is worth mentioning that I am using sessions elsewhere with the -useheaders flag for a REST api; however, sessions did not work until I had an Authentication scheme attached to a Pode Route. Not sure if its related. I ended doing something like this.

Start-PodeServer {
    Enable-PodeSessionMiddleware -Duration 120

    $OAuthScheme = New-PodeAuthScheme -Custom -ScriptBlock {
        return
      }

      $OAuthScheme | Add-PodeAuth -Name 'Login' -ScriptBlock {
        return @{ User = "True" }
      }

    Add-PodeEndpoint -Address * -Protocol http -Port 8081

    Add-PodeRoute -Authentication 'Login' -Method Get -Path '/' -ScriptBlock {
        Get-PodeSessionId | Out-Default
        $WebEvent.Session.Data.Views++
        Write-PodeJsonResponse -Value @{ 'Views' = $WebEvent.Session.Data.Views }
    }
}

Sessions for the REST api did not work with the -useheaders flag until I added an authentication scheme, but in that scenario, I wanted the sessions to be started unauthenticated so I had to make a custom scheme that just doesn't do anything except produce True

The above example takes the custom scheme from the REST API implementation I have, and is modified to show how now a SessionID is displayed. In previous examples this is not working without an auth scheme attached to a route. It should be noted that the SessionID that is returned is definitely changing and not staying the same, but I'm not at all sure how Pode would track this without passing a sessionID along as a query/form parameter or in the header (which is how it is working for my REST API implementation)

Badgerati commented 3 years ago

Hey, apologies for the late response here 🙇 - I was finishing up the release over on Pode.Web, but can get back to doing work on Pode now.

Sessions and Headers requires Authentication, (here), the idea for them is so a user can use a REST call to login, supply the session for other requests, then logout. If you've got a scenario for sessions on a REST API with no authentication, I can look into it!

This is where I'm wondering if something strange is happening to the cookie. When a user logs in via the frontend Pode creates and storage a SessionId with data in-mem, and this SessionId is sent back to the browser as a Set-Cookie header to create the cookie. On subsequent requests from the browser, the browser automatically resends current, non-expired, cookies on all requests. Pode parses these cookies from the request, one hopefully being pode.sid, and uses the value of that to retrieve the Session data.

Now I've released Pode.Web I'll look into this more, one idea I had is what happens if you set the -Duration to something crazy? Like -Duration 604800 so it expires after a week?


Get-PodeSessionId only returns the SessionId if the current session is for an authenticated user, but in hindsight it should probably just just return the current SessionId regardless.

If the request doesn't contain a a pode.sid cookie or header, or the SessionId is invalid, then Pode will regenerate a new one each time. The session is only saved and returned as a cookie/header on response if data is set on the session (like Views++ or the authenticated user)

phatmandrake commented 3 years ago

If you've got a scenario for sessions on a REST API with no authentication, I can look into it.

Well there's an easy way around it so probably not worth the effort to do anything specific about it now at least. 😄

if you set the -Duration to something crazy? Like -Duration 604800 so it expires after a week?

Setting -Duration 18001 is the minimum number that works. 🤯


If the request doesn't contain a a pode.sid cookie or header, or the SessionId is invalid, then Pode will regenerate a new one each time. So is the default behavior that using session middleware generates a cookie unless you specify header?

Badgerati commented 3 years ago

I think I've got it!

If you're able to update the Pode module locally, what happens if you change the following line:

https://github.com/Badgerati/Pode/blob/0f5a522cc71499f86d27ce8165ece295ba31f3fa/src/Private/Sessions.ps1#L202

to be:

$expiry = $Session.Properties.TimeStamp.ToUniversalTime()

instead? :D

I span up a server in Central US when we were first looking at this and CSRF was fine. But your 18001s is bang on UTC -05:00, so figured I'd try again. BOOM, this time it stopped working for me too!

Turns out, the expiry is read from the cookie, but is converted to local time, not UTC. So when it check UTC Now vs Local Now it's expired. Changing that one line to convert to UTC fixed it on my VM 😄


And yes, session middleware will use cookies by default, unless -UseHeader is specified when enabling sessions.

phatmandrake commented 3 years ago

Badgerati for the win!

Ah UTC the DNS of time 🤣 . This should go down in Pode bug history.

Badgerati commented 3 years ago

This should go down in Pode bug history

Hahah, yes definitely! 😂

I've pushed a more full commit to fix this (and the missing Add-PodeEndpoint in the docs!). I'll look at getting this out into a v2.4.2 will some other bugs - rather than waiting for v2.5.0! 😃