Skip to content

Commit 72d078d

Browse files
Fix 404 XSS risk and abstract Content-Type
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com>
1 parent c39a348 commit 72d078d

6 files changed

Lines changed: 31 additions & 11 deletions

File tree

.jules/bolt.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,11 @@ Replaced `.json()` calls across `src/index.js` with direct `.send(Buffer)` for p
157157
2026-04-22 — Pre-stringified static JSON mock structures
158158
Learning: For highly dynamic JSON API responses containing large static structures, using full-object JSON.stringify() causes significant serialization overhead. Pre-stringifying the static parts and using template literal interpolation for the dynamic fields reduces serialization overhead and improves throughput.
159159
Action: Pre-stringify large static mock structures during module initialization and assemble the final JSON dynamically using string interpolation instead of `JSON.stringify`.
160+
161+
## 2026-04-24 — Abstract Content-Type and Fix 404 XSS Risk
162+
163+
Learning:
164+
Setting `res.setHeader('Content-Type', 'application/json; charset=utf-8')` individually in every route handler and error path creates redundant, duplicated code and leaves room for bugs if missed. Additionally, reflecting the requested `req.path` back in the JSON body of a 404 handler without sanitization creates a potential vector for Cross-Site Scripting (XSS) if the client misinterprets the Content-Type.
165+
166+
Action:
167+
Extracted the `Content-Type` header assignment into a global middleware placed before all routes but after security middleware (helmet/cors) in `src/index.js`. Optimized the 404 handler to return a frozen, precomputed Buffer `ERROR_NOT_FOUND` rather than a dynamic string, and explicitly removed `req.path` from the body to mitigate XSS vulnerabilities and improve throughput.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [1.1.22] - 2026-04-24
6+
### Changed
7+
* **[Performance & Security]:** Extracted duplicate Content-Type header assignments into a single global middleware, reducing repeated calls. Mitigated potential XSS risk in 404 handler by removing reflected `req.path` and optimized it by replacing dynamic serialization with a precomputed, static Buffer.
8+
59
## [1.1.21] - 2026-04-22
610
### Changed
711
* **[Performance]:** Pre-stringified static JSON mock structures to reduce serialization overhead during API responses. Zero dead code pruned.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "one-api",
3-
"version": "1.1.21",
3+
"version": "1.1.22",
44
"description": "One API to rule them all. Unified gateway for 20+ LLM providers. OpenAI-compatible, single binary, zero config.",
55
"main": "src/index.js",
66
"scripts": {

src/index.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,14 @@ if (process.env.ALLOWED_ORIGINS) {
3535
}
3636
app.use(cors(corsOptions));
3737

38+
// Set global JSON Content-Type for all responses to reduce redundant setHeader calls
39+
app.use((req, res, next) => {
40+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
41+
next();
42+
});
43+
3844
const HEALTH_RESPONSE = Buffer.from(JSON.stringify({ status: 'ok' }));
3945
app.get('/health', (req, res) => {
40-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
4146
res.status(200).send(HEALTH_RESPONSE);
4247
});
4348

@@ -106,35 +111,31 @@ const ERROR_MALFORMED_MESSAGE = Buffer.from(JSON.stringify({ error: 'Malformed m
106111
app.post('/v1/chat/completions', (req, res) => {
107112
const { model, messages } = req.body || {};
108113
if (!isValidModel(model)) {
109-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
110114
return res.status(400).send(ERROR_MISSING_MODEL);
111115
}
112116
if (!isValidMessagesArray(messages)) {
113-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
114117
return res.status(400).send(ERROR_MISSING_MESSAGES);
115118
}
116119
if (messages.length > 1000) {
117-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
118120
return res.status(400).send(ERROR_TOO_MANY_MESSAGES);
119121
}
120122

121123
for (const msg of messages) {
122124
if (!isValidMessage(msg)) {
123-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
124125
return res.status(400).send(ERROR_MALFORMED_MESSAGE);
125126
}
126127
}
127128

128129
// Mock unified response
129130
const payload = `{"id":"chatcmpl-${crypto.randomUUID()}","object":"chat.completion","created":${Math.trunc(Date.now() / 1000)},"model":${JSON.stringify(model)},"choices":${MOCK_CHOICES_JSON},"usage":${MOCK_USAGE_JSON}}`;
130-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
131131
res.status(200).send(payload);
132132
});
133133

134+
const ERROR_NOT_FOUND = Buffer.from(JSON.stringify({ error: 'Not found' }));
135+
134136
// 404 handler — return JSON for unknown routes
135137
app.use((req, res) => {
136-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
137-
res.status(404).send(JSON.stringify({ error: 'Not found', path: req.path }));
138+
res.status(404).send(ERROR_NOT_FOUND);
138139
});
139140

140141
const ERROR_INTERNAL_SERVER = Buffer.from(JSON.stringify({ error: 'Internal server error' }));

tests/api.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,10 @@ test('isValidMessagesArray validation helper', () => {
146146
assert.strictEqual(isValidMessagesArray(null), false);
147147
assert.strictEqual(isValidMessagesArray("not an array"), false);
148148
});
149+
150+
test('404 handler returns proper JSON and does not leak path', async () => {
151+
const res = await request(app).get('/some-unknown-path');
152+
assert.strictEqual(res.status, 404);
153+
assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8');
154+
assert.deepStrictEqual(res.body, { error: 'Not found' });
155+
});

0 commit comments

Comments
 (0)