Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/33211-hono-security-bump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@objectstack/plugin-hono-server": patch
"@objectstack/plugin-auth": patch
"@objectstack/hono": patch
"@objectstack/trigger-api": patch
"@objectstack/plugin-webhooks": patch
"@objectstack/plugin-approvals": patch
"@objectstack/client": patch
---

fix(deps)!: hono moves to ^4.13.16 and @hono/node-server to ^2.1.6, the releases that fix seven public advisories; a request path holding a backslash is answered 400

Clause-②: yes (narrowing) — `@hono/node-server` 2.1.6 answers `400 Bad Request` to a request whose path (the part before `?`) holds a backslash, before any route runs. Up to 2.1.5 the backslash was rewritten to `/` and the request reached the route that path names. A backslash after `?` is query data and answers as before. No other status, header, body or error text of ours moves; the request-body limit (`413 PAYLOAD_TOO_LARGE`) answers as before.
Metadata-protocol: none
Skills-docs: none — no skill under `skills/**` and no page under `content/docs/**` teaches request-path spelling at the HTTP door; the moved answer is the transport refusing a malformed request line.

<!-- adr-0087: not-required (no-migration-prescription) No schema key, accepted value, metadata type, stored row or export is added, removed, renamed or re-shaped. What moves is the Node transport refusing a request line whose path holds a backslash, so there is nothing for an author or an operator to migrate. -->

**BREAKING** for a client that sends a backslash inside a URL path, such as `GET /api/v1/data\account`: the Node server now answers `400 Bad Request` where it used to route the request as `/api/v1/data/account`. Browsers and HTTP libraries already turn a backslash into `/` (or percent-encode it) before sending, so only a hand-built request line meets this. Send the path with `/`.

The fixed advisories: `hono` GHSA-3gg2-335m-rxc6, GHSA-46wx-v44x-9hh2, GHSA-4h7h-rv7c-7m2v, GHSA-4m76-xfj5-7975 and GHSA-h7mg-cq63-vhpq (fixed in 4.13.16); `@hono/node-server` GHSA-jpvv-r467-5p4w and GHSA-qv5m-9pm6-hqp3 (fixed in 2.1.6). The declared ranges move so that a downstream install, which never sees this workspace's overrides, lands on the fixed releases too. The `@objectstack/hono` peer range on `hono` stays `^4.12.8`.
2 changes: 1 addition & 1 deletion packages/adapters/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
},
"devDependencies": {
"@objectstack/runtime": "workspace:*",
"hono": "^4.13.9",
"hono": "^4.13.16",
"typescript": "^6.0.3",
"vitest": "^4.1.11"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@objectstack/spec": "workspace:*"
},
"devDependencies": {
"@hono/node-server": "^2.1.3",
"@hono/node-server": "^2.1.6",
"@objectstack/driver-sqlite-wasm": "workspace:*",
"@objectstack/hono": "workspace:*",
"@objectstack/metadata-core": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/plugin-approvals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@objectstack/service-messaging": "workspace:*",
"@objectstack/trigger-record-change": "workspace:*",
"@types/node": "^26.6.5",
"hono": "^4.13.9",
"hono": "^4.13.16",
"tsx": "^4.23.15",
"typescript": "^6.0.3",
"vitest": "^4.1.11"
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/plugin-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"@objectstack/types": "workspace:*",
"better-auth": "1.7.12",
"fast-xml-parser": "5.12.0",
"hono": "^4.13.9",
"hono": "^4.13.16",
"jose": "^6.2.12"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/plugin-hono-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
"check:test-typecheck": "tsx ../../../scripts/check-test-typecheck.mts --self-test && tsx ../../../scripts/check-test-typecheck.mts --package packages/plugins/plugin-hono-server --project tsconfig.test.json"
},
"dependencies": {
"@hono/node-server": "^2.1.3",
"@hono/node-server": "^2.1.6",
"@objectstack/core": "workspace:*",
"@objectstack/observability": "workspace:*",
"@objectstack/spec": "workspace:*",
"@objectstack/types": "workspace:*",
"hono": "^4.13.9"
"hono": "^4.13.16"
},
"devDependencies": {
"@types/node": "^26.6.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* A request target whose PATH holds a backslash is refused `400` by the Node
* listener (`@hono/node-server` >= 2.1.6) before any route runs. Up to 2.1.5
* the WHATWG URL parser rewrote the backslash to `/`, so `/api/v1/things\t_1`
* reached the `/api/v1/things/:id` route. The same route without the
* backslash still answers, and a backslash after `?` is query data, not path.
*
* The request line is written on a raw socket: `fetch` would normalise the
* backslash on the client side and never send it.
*/

import { connect } from 'node:net';
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import { HonoHttpServer } from './adapter';

let server: HonoHttpServer;
let port: number;

beforeAll(async () => {
server = new HonoHttpServer(0);
server.get('/api/v1/things/:id', (req, res) => { res.json({ success: true, data: { id: req.params.id, q: req.query.q } }); });
await server.listen(0);
port = server.getPort();
});

afterAll(async () => {
await server.close();
});

function requestLine(target: string): Promise<string> {
return new Promise((resolve, reject) => {
const socket = connect(port, '127.0.0.1');
let text = '';
socket.on('data', (chunk) => { text += chunk.toString('utf8'); });
socket.on('end', () => resolve(text));
socket.on('close', () => resolve(text));
socket.on('error', reject);
socket.write(`GET ${target} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n`);
});
}

describe('a backslash in the request path is refused 400 before any route runs', () => {
it('the route answers without the backslash', async () => {
const answer = await requestLine('/api/v1/things/t_1');
expect(answer).toMatch(/^HTTP\/1\.1 200 /);
expect(answer).toContain('"id":"t_1"');
});

it('the same route with a backslash in its path is answered 400', async () => {
const answer = await requestLine('/api/v1/things\\t_1');
expect(answer).toMatch(/^HTTP\/1\.1 400 /);
expect(answer).not.toContain('"id":"t_1"');
});

it('a backslash after `?` is query data and the route still answers', async () => {
const answer = await requestLine('/api/v1/things/t_1?q=a\\b');
expect(answer).toMatch(/^HTTP\/1\.1 200 /);
expect(answer).toContain('"id":"t_1"');
});
});
2 changes: 1 addition & 1 deletion packages/plugins/plugin-webhooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@objectstack/driver-sql": "workspace:*",
"@objectstack/metadata-core": "workspace:*",
"@types/node": "^26.6.5",
"hono": "^4.13.9",
"hono": "^4.13.16",
"tsx": "^4.23.15",
"typescript": "^6.0.3",
"vitest": "^4.1.11"
Expand Down
2 changes: 1 addition & 1 deletion packages/triggers/trigger-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"devDependencies": {
"@types/node": "^26.6.5",
"hono": "^4.13.9",
"hono": "^4.13.16",
"typescript": "^6.0.3",
"vitest": "^4.1.11"
},
Expand Down
56 changes: 28 additions & 28 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,9 @@ overrides:
# The two declarations (plugin-hono-server, client) move to ^2.1.3 in lockstep
# so a downstream install, which never sees this override, lands on the
# patched line too; @modelcontextprotocol/sdk's ^1.19.9 || ^2.0.5 admits it.
# 2026-11-14 (#33211): GHSA-jpvv-r467-5p4w and GHSA-qv5m-9pm6-hqp3 (5.3 medium
# each) flag 2.1.5, fixed in 2.1.6 — target lifted from ^2.1.3, selector stays
# at the <3.0.0 boundary; the same two declarations move to ^2.1.6 in lockstep.
# brace-expansion, 2026-09-30 (#20769): three MORE advisories, GHSA-6j4f-fj2g-mc7p,
# GHSA-qhr7-859c-m2p7 and GHSA-q2hr-2g5m-vwhr, fixed in 5.0.12 — target lifted from
# ^5.0.9, selector stays at the <6.0.0 major boundary. This floor keeps THIS
Expand All @@ -410,7 +413,7 @@ overrides:
# (objectui's react-router-dom 7.18 pins its own react-router) has no fix to force onto.
'react-router@>=8.0.0 <8.3.0': '^8.3.0'
'@sveltejs/kit@<2.69.1': '^2.69.1'
'@hono/node-server@<3.0.0': '^2.1.3'
'@hono/node-server@<3.0.0': '^2.1.6'
# OSV batch 2026-08 (#5032) — all three name a fixed version, so they are
# upgrades, not exemptions (the osv-scanner.toml route #4965 defines is for
# advisories with NO fix and does not apply here):
Expand Down Expand Up @@ -454,8 +457,13 @@ overrides:
# The declared ranges go to ^4.13.5 in lockstep for the downstream-install
# reason above; the @objectstack/hono PEER range stays ^4.12.8, unchanged,
# for the reason above it.
# 2026-11-14 (#33211): five MORE hono advisories, GHSA-3gg2-335m-rxc6
# (3.4 low), GHSA-46wx-v44x-9hh2, GHSA-4h7h-rv7c-7m2v, GHSA-4m76-xfj5-7975
# and GHSA-h7mg-cq63-vhpq (5.3 each), fixed in 4.13.16 — target lifted
# from ^4.13.5, selector stays at the 5.0.0 boundary. The declared ranges
# go to ^4.13.16 in lockstep; the PEER range stays ^4.12.8 as above.
'fast-uri@<4.0.0': '^3.1.9'
'hono@<5.0.0': '^4.13.5'
'hono@<5.0.0': '^4.13.16'
# OSV 2026-08-07 (#6407) — transitive-only, and the same "it has a fix, so
# take the fix" disposition as the batch above:
# dompurify GHSA-55q2-fjhq-7xh7 (5.1 medium) — an IN_PLACE hook removal
Expand Down
Loading