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
25 changes: 20 additions & 5 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { H3Event } from "./event";
import { MIMES, setResponseStatus } from "./utils";
import {
MIMES,
setResponseStatus,
sanitizeStatusMessage,
sanitizeStatusCode,
} from "./utils";

/**
* H3 Runtime Error
Expand All @@ -21,11 +26,11 @@ export class H3Error extends Error {
"message" | "statusCode" | "statusMessage" | "data"
> = {
message: this.message,
statusCode: this.statusCode,
statusCode: sanitizeStatusCode(this.statusCode, 500),
};

if (this.statusMessage) {
obj.statusMessage = this.statusMessage;
obj.statusMessage = sanitizeStatusMessage(this.statusMessage);
}
if (this.data !== undefined) {
obj.data = this.data;
Expand Down Expand Up @@ -83,15 +88,25 @@ export function createError(
}

if (input.statusCode) {
err.statusCode = input.statusCode;
err.statusCode = sanitizeStatusCode(input.statusCode, err.statusCode);
} else if (input.status) {
err.statusCode = input.status;
err.statusCode = sanitizeStatusCode(input.status, err.statusCode);
}
if (input.statusMessage) {
err.statusMessage = input.statusMessage;
} else if (input.statusText) {
err.statusMessage = input.statusText;
}
if (err.statusMessage) {
// TODO: Always sanitize status message in the next major releases
const originalMessage = err.statusMessage;
const sanitizedMessage = sanitizeStatusMessage(err.statusMessage);
if (sanitizedMessage !== originalMessage) {
console.warn(
"[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future `statusMessage` will be sanitized by default."
);
}
Comment on lines +101 to +108
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks great to me 👌

}

if (input.fatal !== undefined) {
err.fatal = input.fatal;
Expand Down
9 changes: 6 additions & 3 deletions src/event/event.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { H3EventContext } from "../types";
import type { NodeIncomingMessage, NodeServerResponse } from "../node";
import { MIMES } from "../utils";
import { MIMES, sanitizeStatusCode, sanitizeStatusMessage } from "../utils";
import { H3Response } from "./response";

export interface NodeEventContext {
Expand Down Expand Up @@ -45,10 +45,13 @@ export class H3Event implements Pick<FetchEvent, "respondWith"> {
this.res.setHeader(key, value);
}
if (response.status) {
this.res.statusCode = response.status;
this.res.statusCode = sanitizeStatusCode(
response.status,
this.res.statusCode
);
}
if (response.statusText) {
this.res.statusMessage = response.statusText;
this.res.statusMessage = sanitizeStatusMessage(response.statusText);
}
if (response.redirected) {
this.res.setHeader("location", response.url);
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./request";
export * from "./response";
export * from "./session";
export * from "./cors";
export * from "./sanitize";
8 changes: 6 additions & 2 deletions src/utils/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { H3EventContext, RequestHeaders } from "../types";
import { getMethod, getRequestHeaders } from "./request";
import { readRawBody } from "./body";
import { splitCookiesString } from "./cookie";
import { sanitizeStatusMessage, sanitizeStatusCode } from "./sanitize";

export interface ProxyOptions {
headers?: RequestHeaders | HeadersInit;
Expand Down Expand Up @@ -66,8 +67,11 @@ export async function sendProxy(
headers: opts.headers as HeadersInit,
...opts.fetchOptions,
});
event.node.res.statusCode = response.status;
event.node.res.statusMessage = response.statusText;
event.node.res.statusCode = sanitizeStatusCode(
response.status,
event.node.res.statusCode
);
event.node.res.statusMessage = sanitizeStatusMessage(response.statusText);

for (const [key, value] of response.headers.entries()) {
if (key === "content-encoding") {
Expand Down
20 changes: 11 additions & 9 deletions src/utils/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Socket } from "node:net";
import { createError } from "../error";
import type { H3Event } from "../event";
import { MIMES } from "./consts";
import { sanitizeStatusCode, sanitizeStatusMessage } from "./sanitize";

const defer =
typeof setImmediate !== "undefined" ? setImmediate : (fn: () => any) => fn();
Expand All @@ -27,7 +28,7 @@ export function send(event: H3Event, data?: any, type?: string): Promise<void> {
* @param code status code to be send. By default, it is `204 No Content`.
*/
export function sendNoContent(event: H3Event, code = 204) {
event.node.res.statusCode = code;
event.node.res.statusCode = sanitizeStatusCode(code, 204);
// 204 responses MUST NOT have a Content-Length header field (https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2)
if (event.node.res.statusCode === 204) {
event.node.res.removeHeader("content-length");
Expand All @@ -41,15 +42,13 @@ export function setResponseStatus(
text?: string
): void {
if (code) {
event.node.res.statusCode = code;
event.node.res.statusCode = sanitizeStatusCode(
code,
event.node.res.statusCode
);
}
if (text) {
// Allowed characters: horizontal tabs, spaces or visible ascii characters: https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2
event.node.res.statusMessage = text.replace(
// eslint-disable-next-line no-control-regex
/[^\u0009\u0020-\u007E]/g,
""
);
event.node.res.statusMessage = sanitizeStatusMessage(text);
}
}

Expand All @@ -68,7 +67,10 @@ export function defaultContentType(event: H3Event, type?: string) {
}

export function sendRedirect(event: H3Event, location: string, code = 302) {
event.node.res.statusCode = code;
event.node.res.statusCode = sanitizeStatusCode(
code,
event.node.res.statusCode
);
event.node.res.setHeader("location", location);
const encodedLoc = location.replace(/"/g, "%22");
const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`;
Expand Down
23 changes: 23 additions & 0 deletions src/utils/sanitize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Allowed characters: horizontal tabs, spaces or visible ascii characters: https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2
// eslint-disable-next-line no-control-regex
const DISALLOWED_STATUS_CHARS = /[^\u0009\u0020-\u007E]/g;

export function sanitizeStatusMessage(statusMessage = ""): string {
return statusMessage.replace(DISALLOWED_STATUS_CHARS, "");
}

export function sanitizeStatusCode(
statusCode: string | number,
defaultStatusCode = 200
): number {
if (!statusCode) {
return defaultStatusCode;
}
if (typeof statusCode === "string") {
statusCode = Number.parseInt(statusCode, 10);
}
if (statusCode < 100 || statusCode > 999) {
return defaultStatusCode;
}
return statusCode;
}