hono icon indicating copy to clipboard operation
hono copied to clipboard

prettyJSON throws on invalid query parameters

Open lincolnremi opened this issue 10 months ago • 6 comments

What version of Hono are you using?

4.7.2

What runtime/platform is your app running on? (with version if possible)

NodeJS v20.16.0

What steps can reproduce the bug?

prettyJSON calls HonoRequest.query() without a try clause, so when an invalid query parameter is passed, it throws the URIError: URI Malformed error.

Here is the code:

import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { prettyJSON } from "hono/pretty-json";

const app = new Hono()
app.use("*", prettyJSON());

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

serve({
  fetch: app.fetch,
  port: 3000
}, (info) => {
  console.log(`Server is running on http://localhost:${info.port}`)
})

This code was obtained by running npm create hono@latest and adding two lines for importing and using prettyJSON.

What is the expected behavior?

It should not throw an error

What do you see instead?

Making requests to the server throws the error as follows:

lincolnbergeson@Lincolns-MacBook-Pro-2 ~ % curl 'localhost:3000'
Hello Hono!%
lincolnbergeson@Lincolns-MacBook-Pro-2 ~ % curl 'localhost:3000?%E0%A4%A'
Internal Server Error%

And the server logs:

Server is running on http://localhost:3000
URIError: URI malformed
    at decodeURIComponent (<anonymous>)
    at _decodeURI (file:///Users/lincolnbergeson/Documents/GitHub/hono-demo/node_modules/hono/dist/utils/url.js:132:38)
    at _getQueryParam (file:///Users/lincolnbergeson/Documents/GitHub/hono-demo/node_modules/hono/dist/utils/url.js:171:14)
    at HonoRequest.query (file:///Users/lincolnbergeson/Documents/GitHub/hono-demo/node_modules/hono/dist/request.js:41:12)
    at prettyJSON2 (file:///Users/lincolnbergeson/Documents/GitHub/hono-demo/node_modules/hono/dist/middleware/pretty-json/index.js:5:26)
    at dispatch (file:///Users/lincolnbergeson/Documents/GitHub/hono-demo/node_modules/hono/dist/compose.js:22:23)
    at file:///Users/lincolnbergeson/Documents/GitHub/hono-demo/node_modules/hono/dist/compose.js:5:12
    at file:///Users/lincolnbergeson/Documents/GitHub/hono-demo/node_modules/hono/dist/hono-base.js:195:31
    at #dispatch (file:///Users/lincolnbergeson/Documents/GitHub/hono-demo/node_modules/hono/dist/hono-base.js:205:7)
    at fetch (file:///Users/lincolnbergeson/Documents/GitHub/hono-demo/node_modules/hono/dist/hono-base.js:208:26)

Additional information

No response

lincolnremi avatar Feb 24 '25 18:02 lincolnremi

This patch should work:

diff --git a/src/middleware/pretty-json/index.ts b/src/middleware/pretty-json/index.ts
index bc198e58..a5428f7d 100644
--- a/src/middleware/pretty-json/index.ts
+++ b/src/middleware/pretty-json/index.ts
@@ -40,7 +40,15 @@ interface PrettyOptions {
 export const prettyJSON = (options?: PrettyOptions): MiddlewareHandler => {
   const targetQuery = options?.query ?? 'pretty'
   return async function prettyJSON(c, next) {
-    const pretty = c.req.query(targetQuery) || c.req.query(targetQuery) === ''
+    let pretty;
+    try {
+      pretty = c.req.query(targetQuery) || c.req.query(targetQuery) === ''
+    } catch (e) {
+      // Ignore URIError caused by invalid query parameter
+      if (!(e instanceof URIError)) {
+        throw e;
+      }
+    }
     await next()
     if (pretty && c.res.headers.get('Content-Type')?.startsWith('application/json')) {
       const obj = await c.res.json()

I can put this into a PR & test if the maintainers are open to it

lincolnremi avatar Feb 24 '25 19:02 lincolnremi

Hi @lincolnremi Thank you for the issue! Good catch. But please wait a bit.

Hey @usualoma ! What do you think of the following?

Not only a Pretty JSON middleware case, but I wonder whether it is correct behavior to get a URIError: URI malformed error if you passed an invalid query to c.req.query().

In the case of Express, instead of throwing an error, the invalid query is treated as a string.

The app code:

const express = require('express')
const app = express()

app.get('/', (req, res) => {
  const foo = req.query.foo
  res.send(`foo: ${foo}`)
})

app.listen(3000, () => {
  console.log('Server is running on port 3000')
})

The results:

$ curl 'localhost:3000?foo=%E3%81%82'
foo: あ%
$ curl 'localhost:3000?foo=%E3%81%8'
foo: %E3%81%8%

But the Hono app:

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  const foo = c.req.query('foo')
  return c.text(`foo: ${foo}`)
})

export default app

Perhaps we should follow the same spec as Express? The results:

$ curl 'localhost:3000?foo=%E3%81%82'
foo: あ%
$ curl 'localhost:3000?foo=%E3%81%8'
Internal Server Error%

yusukebe avatar Mar 02 '25 07:03 yusukebe

Express relies on query parser libraries such as qs. As suggested by someone before, it might be a good idea to add a custom query parser option.

EdamAme-x avatar Mar 02 '25 09:03 EdamAme-x

I also think it would be good if the result here was the same as with the express.

$ curl 'localhost:3000?foo=%E3%81%82'
foo: あ
$ curl 'localhost:3000?foo=%E3%81%8'
foo: %E3%81%8

I think it would be better if users defined and used their own custom query parser. I think it is more likely that users want to use their own parser because they want to define their own type, so I think it would be better if they defined it themselves rather than incorporating it into the hono.

const foo = new myCustomQuery(c).get(‘foo’)

usualoma avatar Mar 02 '25 12:03 usualoma

@EdamAme-x @usualoma

Thank you for the comments!

Regarding the custom query parser, I prefer to use the syntax @usualoma suggested.

myCustomQuery(c).get(‘foo’)

But we should discuss implementing the same behavior with Express for the default. Of course, the user can change the behavior with the custom query parser, but almost all users do not use it, and the default behavior is important.

We can treat the difference between Hono's and Express's as a bug or unexpected behavior. I think we should make it so that the error is not thrown if c.req.query parses invalid strings since it can cause vulnerability by intentionally sending invalid values. It's unexpected behavior, so I think it would be better that we should fix it.

I want to know what you think.

yusukebe avatar Mar 02 '25 21:03 yusukebe

Hi @yusukebe. I'm sorry for not getting back to you sooner.

Yes, I also think it would be preferable to make it behave the same as Express. I believe the following changes would achieve this.

diff --git a/src/utils/url.ts b/src/utils/url.ts
index 27604022..5772adae 100644
--- a/src/utils/url.ts
+++ b/src/utils/url.ts
@@ -216,7 +216,7 @@ const _decodeURI = (value: string) => {
   if (value.indexOf('+') !== -1) {
     value = value.replace(/\+/g, ' ')
   }
-  return value.indexOf('%') !== -1 ? decodeURIComponent_(value) : value
+  return value.indexOf('%') !== -1 ? tryDecode(value, decodeURIComponent_) : value
 }
 
 const _getQueryParam = (

usualoma avatar Mar 22 '25 10:03 usualoma

This issue was fixed with #4110. You can use the latest version 4.8.0 including the fix! Thanks.

yusukebe avatar Jun 19 '25 08:06 yusukebe