Morgbn / nuxt-csurf

Nuxt Cross-Site Request Forgery (CSRF) Prevention
https://nuxt-csurf.vercel.app
MIT License
77 stars 16 forks source link

Server rendered component useCsrf() returns undefined #20

Closed outofthisworld closed 10 months ago

outofthisworld commented 11 months ago

@Morgbn

I'm trying to use this module but I'm having an issue retrieving the token on the server during first SSR render...

Heres the example:

<script lang="ts" setup>
const { csrf } = useCsrf();
console.log(csrf); // prints undefined

const result = await useCsrfFetch("/api/test", {
  method: "post",
  body: "",
});

console.log("result :: ", result); // error result running on server (Invalid CSRF token, as its undefined.)

 const result = await $fetch('/api/test', {
   method:'post',
   headres:{
      'csrf-token': csrf
   }, 
    body:''
}); // fails... csrf token invalid...undefined on server

</script>

Basically, CSRF token is always undefined on the server which isn't great... because it means the fetch has to run on the client side. You can see looking at the page data that the server request always fails.

I found the source of the issue looking at the useCsrf() implementation, it hasn't accounted for SSR scenarios and if process.server=true then the composable always returns null for csrf

The next problem is that the cookie is storing only the random secret, and its not currently possible to get the token from the event using useRequestEvent() composable. Additionally, no context, or headers or anything are set in the nitro plugin so that we can retrieve the token server side.

The nitro plugin currently registers the hook render:html unfortunately the html is already rendered so we can't attach anything to the event here as our rendering page wont get it. My proposed solution to get this working would be the following:

The nitro plugin should be like this:

export default defineNitroPlugin((nitroApp) => {
  const csrfConfig = useRuntimeConfig().csurf;
  const cookieKey = csrfConfig.cookieKey;

  nitroApp.hooks.hook("request", async (event)=>{
    let secret = getCookie(event, cookieKey);
    if (!secret) {
      secret = csrf.randomSecret();
      setCookie(event, cookieKey, secret, csrfConfig.cookie);
    }
    const csrfToken = await csrf.create(secret, await useSecretKey(csrfConfig), csrfConfig.encryptAlgorithm);
    // Attach the generated token to the event context and we can access it
   // In our pages...
    event.context.csrfToken = csrfToken;
  });

  nitroApp.hooks.hook("render:html", async (html, { event }) => {
    if(event.context.csrfToken){
      return;
    }
    html.head.push(`<meta name="csrf-token" content="${event.context.csrfToken}">`);
  });
});

Then we can create something like this:

function useCsrfPatched() {
  if (process.client) {
    return useCsrf();
  } else {
    return { csrf: useRequestEvent()?.context?.csrfToken };
  }
}

Which would allow this to work:

const { csrf } = useCsrfPatched();

const result = await useFetch("/api/test", {
  method: "POST",
  body: "",
  headers: {
    "csrf-token": csrf!,
  },
});

At the user level it's not even possible to fix this, for example:

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook("render:html", async (html, { event }) => {
    const csrfTokenMeta = html.head.find((head) =>
      head.includes('name="csrf-token"')
    );
    const match = csrfTokenMeta && csrfTokenMeta.match('content="(.*)"');

    if (match) {
      const [_, csrfToken] = match;

      // Context is set AFTER page has been rendered, so never accessible in pages..
      event.context.csrf = csrfToken;
    }
  });
});

This was probably never encountered, because quite often POST requests aren't being made from server side to a CSRF protected route, typically GET requests are issued which aren't commonly CSRF protected. However, I can see this has use cases for instance:

useFetch('/pageView, { body:SomeUserContext, method:'post', headers: { csrf } })

github-actions[bot] commented 10 months ago

:tada: This issue has been resolved in version 1.4.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket: