Solution : Hono CSRF middleware blocking valid formData requests.
What version of Hono are you using?
4.7.5
What runtime/platform is your app running on? (with version if possible)
Bun
What steps can reproduce the bug?
Use CSRF middleware with expected origins. Create an api to receive formData requests.
import { Hono } from 'hono'
import { csrf } from 'hono/csrf'
const app = new Hono()
app.use('*', csrf({ origin: ["http://your-domain.com", "http://localhost"] }))
app.post('/ping', async (c) => {
const body = await c.req.parseBody()
c.status(200)
return c.json({ success: true, message: `Hi ${body.name}` })
})
export default app
Create a test for the api
import { describe, it, mock, test, expect } from 'bun:test';
import app from './index';
it('Testing api using formData', async () => {
const formData = new FormData();
formData.append('name', 'John Doe');
const res = await app.request('/ping', {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
'Origin': 'http://localhost',
}
});
expect(res.status).toBe(200);
})
Test will fail with error 403 despite the correct Origin.
What is the expected behavior?
Test should Pass with response status 200.
What do you see instead?
Hono api will throw an error 403.
Additional information
Hono CSRF middleware blocking valid formData requests despite whitelisting the origin. I am facing this problem for very long time on our Android and iOS apps but it magically works fine on web browsers. On website we don't set any explicit headers for content-type or origin but it still works. While on iOS and Android apps our formData requests are always blocked by CSRF middleware whether we set or not set explicit headers for content-type and origin.
Solution
You have to explicitly set the Origin header and remove the Content-Type header in your request.
const res = await app.request('/ping', {
method: 'POST',
body: formData,
headers: {
'Origin': 'http://localhost',
}
});
If both Origin header and Content-Type header present then request will fail :
const res = await app.request('/ping', {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
'Origin': 'http://localhost',
}
});
Its a bug that needs to be fixed by Hono Dev Team.
Hi @vinay-khatri.
From your report, it seems that Origin headers are not being added to requests from Android and iOS apps.
If it is difficult to add the Origin header, then in the first place, the CSRF middleware is intended to prevent cross-site vulnerability issues via web browsers, not for apps that can create arbitrary requests. So, I think it is better not to apply CSRF to requests from apps, using "combine" middleware, etc.
Hi @usualoma
I have shown with the example that adding Origin header doesn't fix the problem unless you remove the Content-Type header... CSRF middleware is doing some dirty checking... Origin header along with Content-Type header should also pass through the CSRF middleware if they are valid.
For example this will fail when both Origin and Content-Type is set:
const res = await app.request('/ping', {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
'Origin': 'http://localhost',
}
});
But this will pass when Content-Type is removed and only Origin is set :
const res = await app.request('/ping', {
method: 'POST',
body: formData,
headers: {
'Origin': 'http://localhost',
}
});
Hi @vinay-khatri. Thank you for your response.
By the way, is the following code actually working? I think you need something like multipart/form-data; boundary=a-boundary for ‘multipart/form-data’. (I think the code as it is will result in a 500 error).
Could you create a small project with actual working code to reproduce the problem?
const res = await app.request('/ping', {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
'Origin': 'http://localhost',
}
});
I ran into a similar issue when writing a fetch interceptor. Spreading an object of type HeadersInit breaks Hono's c.req.valid('json') parsing. Not sure if this is expected behavior, but here’s what worked for me:
// 🚫 This breaks c.req.valid('json') in Hono. It will return undefined
// I assume it's related to the serialization of mutated headers
const response = await originalFetch(input, {
...init,
headers: {
...init.headers,
['Custom-Header-Key']: 'dummy',
},
});
// ✅ This preserves c.req.valid('json')
// Convert to a plain object if it's a Headers instance
const newHeaders: Record<string, string> = (
init.headers instanceof Headers
? Object.fromEntries(init.headers.entries())
: (init.headers ?? {})
) as Record<string, string>;
newHeaders['Custom-Header-Key'] = 'dummy';
const response = await originalFetch(input, {
...init,
headers: newHeaders,
});