gofiber / fiber

⚡️ Express inspired web framework written in Go
https://gofiber.io
MIT License
33.33k stars 1.64k forks source link

🤗 [Question]: 403 forbidden on POST CSRF request #2727

Closed romanian-bag-void closed 10 months ago

romanian-bag-void commented 10 months ago

Question Description

Hi, I've set up the frontend and backend to both use HTTPS. I'm trying to create the Synchronizer Token Pattern CSRF Login pre-session using the CSRF middleware. I have a GET response that sets up the CSRF and Session tokens but the POST fails. The header seems to be identically named, the headers are there, both frontend and backend (AllowHeaders: "content-type,x-csrf-token"), but I get this 403 error.

Code attached. Am I missing something?

Code Snippet (optional)

Backend code:

presessionConfig := csrf.Config{
        Session:        sessionManager,
        Storage:        memoryStorage, 
        KeyLookup:      "header:X-CSRF-Token", // I feel that here's the issue, but I don't see how
        CookieName:     "__Host-csrf",
        CookieSameSite: "Lax",
        CookieSecure:   true,
        CookieHTTPOnly: false,
        ContextKey:     "csrf", // and that this might interfere somehow
        ErrorHandler:   csrfErrorHandler,
        Expiration:     30 * time.Minute,
    }
    presessionMiddleware := csrf.New(presessionConfig)

    app.Get("/login", presessionMiddleware, func(c *fiber.Ctx) error {
        csrfToken, ok := c.Locals("csrf").(string)
        if !ok {
            return c.SendStatus(fiber.StatusInternalServerError)
        }

        sessionTokenUUID := uuid.New()
        sessionTokenString := sessionTokenUUID.String()
        err := memoryStorage.Set(sessionTokenString, []byte("tempTest"), 120*time.Second)
        if err != nil {
            return err
        }

        return c.Status(fiber.StatusOK).JSON(fiber.Map{
            "csrf":    csrfToken,
            "session": sessionTokenString,
        })
    })

    app.Post("/login", presessionMiddleware, func(c *fiber.Ctx) error {
        ... stuff happens here. Nothing to throw a 403 error here, at all. ...
        return c.SendStatus(200)
    })

Frontend request:

export async function getUser(data: { email: string; password: string, csrf: string }): Promise<any> {
    const response = await fetch(
        "https://localhost:3000/login",
        {
            method: "POST",
            mode: "cors",
            credentials: "include",
            headers: {
                "Content-Type": "application/json",
                "X-CSRF-Token": data.csrf,
                cookie: "__Host-csrf="+data.csrf
            },
            body: JSON.stringify(data)
        },
    )
    return response.json()
}

Checklist:

welcome[bot] commented 10 months ago

Thanks for opening your first issue here! 🎉 Be sure to follow the issue template! If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

ReneWerner87 commented 10 months ago

@sixcolors can you help here

sixcolors commented 10 months ago

@romanian-bag-void did you take a look at the network request with dev tools? Is it sending the token?

the header isn’t the default capitalization but you’ve set it the same so that shouldn’t matter. And since you are accessing the token via the cookie the context key shouldn’t be needed so that’s probably not the issue either.

If the front end is running in a browser you don’t need to set the cookie header.

sixcolors commented 10 months ago

@romanian-bag-void actually I might see the issue here. https://localhost:3000/login Is your front end running server side (next, nuxt, svelte kit etc)?

If so you will need to add a Referer header that says it comes from localhost. https://docs.gofiber.io/api/middleware/csrf#referer-checking

I’m on my phone traveling right now, but I think this should work:


const url = 'https://localhost:3000/api/data'; // Replace with your actual localhost URL
const referer = 'https://localhost:3000'; // Set Referer to localhost:3000

fetch(url, {
  method: 'GET',
  headers: {
    'Referer': referer,
    // other headers if needed
  },
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

alternatively since you are using localhost you could just use http…

another question: is the api is only available to localhost on the backend and not accessible to the outside world?

romanian-bag-void commented 10 months ago

First, I completely forgot to add that I have CORS added as well. My bad:

app.Use(cors.New(cors.Config{ AllowOrigins: "https://localhost:5173", AllowHeaders: "content-type,x-csrf-token", AllowMethods: "*", AllowCredentials: true, })) In addition, with the error I get the following error in the console whilst running the backend:

CSRF Error: referer invalid Request: /login From: 127.0.0.1 17:52:29 | 403 | 14.727µs | 127.0.0.1 | POST | /login

@sixcolors yes, the token is being sent. I'm using React. The API is local, the reason why I'm doing the whole HTTPS thing is because I will deploy this with HTTPS and want to use it as a temporary testing ground, and I'm already on a Linux machine.

Port 5173 is frontend, port 3000 is backend.

I tried adding the referrer in the request, but that didn't change anything. Adding what I suppose is the default referrer-policy didn't help out either, I still get a 403:

export async function getUser(data: { email: string; password: string, csrf: string }): Promise<any> { const response = await fetch( "https://localhost:3000/login", { method: "POST", mode: "cors", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": data.csrf, cookie: "__Host-csrf="+data.csrf }, referrer: "https://localhost:3000/login", referrerPolicy: "strict-origin-when-cross-origin", body: JSON.stringify(data) }, ) return response.json() }

sixcolors commented 10 months ago

If it’s react, you’re request is Cross origin because you’re using different ports for the host.

Browsers will not let js set a Referer header, my example prior only works in node js.

You would need to either put them behind a reverse proxy or use HTTP.

sixcolors commented 10 months ago

Here is an incomplete example that might help: https://github.com/sixcolors/gofiber-react-session-csrf-example

sixcolors commented 10 months ago

@romanian-bag-void you can also make a custom error handler for csrf to see what error is actually causing the forbidden

example: https://github.com/gofiber/recipes/blob/master/csrf-with-session/main.go

romanian-bag-void commented 10 months ago

I'll see what I can find out and post it here, most likely tomorrow. I'll first see how I can debug more using a custom CSRF error handler, maybe that will give me a better hint.

romanian-bag-void commented 10 months ago

This is what some basic debugging says:

CSRF Error: referer invalid Request: /login From: 127.0.0.1 Params: map[] ResponseHeaders: map[ Accept:[/] Accept-Encoding:[gzip, deflate, br] Accept-Language:[en-US,en;q=0.5] Connection:[keep-alive] Content-Length:[112] Content-Type:[application/json] Cookie:[Host-session=dcf5c3df-76b3-416a-b84f-4a6e702d6768; Host-csrf=ef841f67-bf1f-4775-9d5e-498127a24b35] Host:[localhost:3000] Origin:[https://localhost:5173] Referer:[https://localhost:5173/] Sec-Fetch-Dest:[empty] Sec-Fetch-Mode:[cors] Sec-Fetch-Site:[same-site] User-Agent:[Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0] X-Csrf-Token:[ef841f67-bf1f-4775-9d5e-498127a24b35] ] RequestHeaders: map[ Access-Control-Allow-Credentials:[true] Access-Control-Allow-Origin:[https://localhost:5173] Content-Type:[text/plain; charset=utf-8] Vary:[Origin] ]

the debugging part. Don't know what else I should add.

    csrfErrorHandler := func(c *fiber.Ctx, err error) error {
    fmt.Printf("CSRF Error: %v Request: %v From: %v\n Params: %v\n ResponseHeaders: %v\n RequestHeaders: %v\n ", err, c.OriginalURL(), c.IP(), c.AllParams(), c.GetReqHeaders(), c.GetRespHeaders())
    return c.Status(fiber.StatusForbidden).SendString("403 Forbidden")
}
sixcolors commented 10 months ago

It’s an invalid Referer which is what I mentioned earlier. https://localhost:5173 to https://localhost:3000 is a cross origin post. The middleware is functioning as expected.

I suggest using http for local dev with react.

Or

You could put it behind a reverse proxy so that they come from the same host (and port), per the example I shared earlier. The example isn’t setup for dev debugging react yet, but I’ll be adding that shortly.

romanian-bag-void commented 10 months ago

Yeah, I'll go with a reverse proxy.

Will setup that reverse proxy and then report it here the next days, so that if someone hits the same wall like me doesn't repeat my mistakes.

sixcolors commented 10 months ago

@romanian-bag-void are you okay if we close this issue?

romanian-bag-void commented 10 months ago

@sixcolors haven't had much success in setting up the reverse proxy, decided to try a temporary live environment to test things up.

I'll report back in a few more days.

sixcolors commented 10 months ago

@sixcolors haven't had much success in setting up the reverse proxy, decided to try a temporary live environment to test things up.

I'll report back in a few more days.

I did update https://github.com/sixcolors/gofiber-react-session-csrf-example which should give you a decent example of what you need for a React front-end with a GoFiber api behind a reverse proxy, including remote debugging and hot-reload support in dev.

romanian-bag-void commented 10 months ago

Deployed it in the live environment, the 403 error went away, gave me some other issues ("invalid character '0' after object key:value pair") but that's a different subject that I'll try to investigate on my own for the moment. Basically, there's no more 403 issue after deploying it.

Closing it since the subject is redundant now.