h3
Version:
Minimal H(TTP) framework built for high performance and portability.
176 lines (107 loc) • 8.43 kB
Markdown
# Security
> H3 security utilities.
## Authentication
### `basicAuth(opts)`
Create a basic authentication middleware.
**Example:**
```ts
import { H3, serve, basicAuth } from "h3";
const auth = basicAuth({ password: "test" });
app.get("/", (event) => `Hello ${event.context.basicAuth?.username}!`, [auth]);
serve(app, { port: 3000 });
```
### `requireBasicAuth(event, opts)`
Apply basic authentication for current request.
**Example:**
```ts
import { defineHandler, requireBasicAuth } from "h3";
export default defineHandler(async (event) => {
await requireBasicAuth(event, { password: "test" });
return `Hello, ${event.context.basicAuth.username}!`;
});
```
## Session
### `clearSession(event, config)`
Clear the session data for the current request.
### `getSession(event, config)`
Get the session for the current request.
A request without a session gets a new one initialized in memory only — no `Set-Cookie` is issued until something is stored with {@link updateSession}, so reading the session (an auth check, for example) does not start one for anonymous visitors. Its `id` is therefore only stable across requests once the session has been written; use {@link useSession} to start one eagerly.
### `sealSession(event, config)`
Encrypt and sign the session data for the current request.
### `unsealSession(_event, config, sealed)`
Decrypt and verify the session data for the current request.
### `updateSession(event, config, update?)`
Update the session data for the current request.
### `useSession(event, config)`
Create a session manager for the current request.
Starts a session if the request does not carry one, persisting it so its id is stable across requests. Use {@link getSession} to read a session without starting one.
## Fingerprint
### `getRequestFingerprint(event, opts)`
Get a unique fingerprint for the incoming request.
## CORS
### `appendCorsHeaders(event, options)`
Append CORS headers to the response.
### `appendCorsPreflightHeaders(event, options)`
Append CORS preflight headers to the response.
### `handleCors(event, options)`
Handle CORS for the incoming request.
If the incoming request is a CORS preflight request, it will append the CORS preflight headers and send a 204 response.
If return value is not `false`, the request is handled and no further action is needed.
**Example:**
```ts
const app = new H3();
app.all("/", async (event) => {
const corsRes = handleCors(event, {
origin: "*",
preflight: {
statusCode: 204,
},
methods: "*",
});
if (corsRes !== false) {
return corsRes;
}
// Your code here
});
```
### `isCorsOriginAllowed(origin, options)`
Check if the origin is allowed.
### `isPreflightRequest(event)`
Check if the incoming request is a CORS preflight request.
## Path
### `isCanonicalPath(path, opts?)`
Whether `path` is already canonical under `opts` — i.e. {@link resolveDotSegments} would return it unchanged. Exact in both directions: `true` if and only if `resolveDotSegments(path, opts) === path`.
This is the resolver's own fast-path guard, exported so a caller that canonicalizes on a hot path (per-request scope or rule matching) can skip the call — and any work derived from it — without keeping its own copy of what the resolver decodes. Such a copy goes stale silently, and a missed canonicalization in a scope check is a bypass, not a perf bug.
Pass the same options as the later {@link resolveDotSegments} call, or stricter ones: `decodeSlashes`/`mergeSlashes` only add triggers, so `true` with both enabled implies `true` in every mode. Checking one mode and resolving in another voids the guarantee.
Takes a bare pathname. Like the resolver, it has no notion of a query or hash and scans one as if it were path, so `/a?next=/../b` is reported non-canonical (and would resolve to `/b`).
### `normalizeRoute(route)`
Normalize a route pattern into the canonical form h3 registers it under — the same shape as the `event.url.pathname` it will be matched against.
`app.on()`, `app.use(route, …)`, `app.mount()` and `removeRoute()` all apply this to the pattern they receive. Use it when registering patterns into a router of your own (e.g. a build-time compiled rou3 router) that is then matched against h3's `event.url.pathname`, so both sides agree on the string — a pattern that normalized differently could leave a route reachable while a guard registered with the same source string matches nothing.
A leading `/` is added if missing (`about` → `/about`), characters a request pathname always carries percent-encoded are encoded (`/café/**` → `/caf%C3%A9/**`), needless escapes are decoded the way h3 decodes them in the request pathname (`/%40handle` → `/@handle`; `%2F` and `%25` stay encoded), and `.`/`..` segments are resolved (`/a/b/../c` → `/a/c`). rou3 pattern syntax (`?`, `{`, `}`, `^`, `\`) is left verbatim — spell one percent-encoded to match it literally.
Idempotent. Throws on an absolute URL (`http://…`): a route pattern is a pathname, never a URL.
**Example:**
```ts
normalizeRoute("/について/**"); // "/%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/**"
```
### `resolveDotSegments(path, opts?)`
Resolve `.` and `..` segments in a path, without ever escaping above the root `/`. The result is always an absolute path with a single leading `/`, so it can never be protocol-relative (`//host`).
Also decodes percent-encoded dot segments at any `%25`-nesting depth (`%2e`, `%252e`, ...) and normalizes `\` to `/`, so encoded or backslash-based traversal (e.g. `%2e%2e/`, `..\..\`) is caught the same way as a literal `../`.
`%2f`/`%5c` (encoded path separators) are left untouched by default — see {@link ResolveDotSegmentsOptions.decodeSlashes}.
Only `.`/`..` resolution and the decodes above alter the string; every other percent-encoding (`%20`, non-ASCII, `%3A`, and any `%2e` not forming a whole segment) is left intact, so the result stays in the same representation as `event.url.pathname` and matches routes/rules consistently. A trailing `.`/`..` resolves to a directory and keeps its trailing slash (`/a/b/..` -> `/a/`, `/a/.` -> `/a/`), per RFC 3986 §5.2.4 and matching what a WHATWG/nginx downstream resolves — so a scope check sees the directory form, not its file-form sibling. Interior empty segments are preserved (`/a//b` stays `/a//b`) — like WHATWG, this never merges slashes, so empty segments survive rather than collapsing. The one exception is a <u>leading</u> run: it is always clamped to a single `/` (WHATWG would keep `//host`), so only the leading slash is guaranteed single and a consumer doing exact prefix matching should normalize its allowlist the same way. To collapse interior runs too (the reading a slash-merging downstream resolves), see {@link ResolveDotSegmentsOptions.mergeSlashes}.
## Route params
Route params reach your handler in the form they had in the URL path — percent-encoded. `getRouterParams(event, { decode: true })` (and `getValidatedRouterParams` with the same option) applies **one** decode pass, not a full normalization:
- Encoded path separators (`%2f`, `%5c`, at any `%25`-nesting depth: `%252f`, `%25252f`, ...) are **never** decoded. A raw `/` or `\` can never appear in a param that the router matched as one segment, so a param cannot silently gain a path boundary that routing and middleware never saw.
- Every other escape decodes exactly one level. Because `%25` is itself an escape, `%25XX` decodes to the literal text `%XX` — so the result can still contain percent-escapes.
```ts
app.get("/files/**:rest", (event) => {
// GET /files/%252e%252e/x
getRouterParams(event); // { rest: "%252e%252e/x" }
getRouterParams(event, { decode: true }); // { rest: "%2e%2e/x" }
// GET /files/%2500
getRouterParams(event, { decode: true }); // { rest: "%00" }
// GET /files/a%252fb — separators stay encoded at every depth
getRouterParams(event, { decode: true }); // { rest: "a%252fb" }
});
```
> [!IMPORTANT]
Do not decode the returned value again. A second `decodeURIComponent` turns `%2e%2e/x` into `../x` and `%00` into a NUL byte — traversal and control characters that were not visible to routing or to any pathname-based middleware. Validate the value as returned, and if it will be used as a filesystem or upstream path, resolve it with [`resolveDotSegments`](#resolvedotsegmentspath-opts) rather than by decoding further.