Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 4 additions & 4 deletions docs/api-reference/veryfront/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ await server.fetch(new Request("https://example.com/health"));

| Name | Description | Source |
| -------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `HOSTED_ENVIRONMENT_NAMES` | Environment labels that `{slug}.{environment}.veryfront.com` actually routes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L44) |
| `HOSTED_ENVIRONMENT_NAMES` | Environment labels that `{slug}.{environment}.veryfront.com` actually routes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L47) |
| `ReloadNotifier` | Render reload notifier. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/reload-notifier.ts#L146) |

### Functions
Expand All @@ -50,8 +50,8 @@ await server.fetch(new Request("https://example.com/health"));
| `createHandler` | Create a Veryfront request handler for development or production. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L238) |
| `createVeryfrontServer` | Create veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L157) |
| `gracefullyShutdownProductionServer` | Enter lame-duck mode, mark readiness false, drain tracked requests and SSE response bodies, and stop a production server process. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/graceful-shutdown.ts#L218) |
| `isHostedEnvironmentName` | Whether `{slug}.{name}.veryfront.com` is a host the platform can route. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L58) |
| `parseProjectDomain` | Extract project slug and branch from domain/host header | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L122) |
| `isHostedEnvironmentName` | Whether `{slug}.{name}.veryfront.com` is a host the platform can route. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L61) |
| `parseProjectDomain` | Extract project slug and branch from domain/host header | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L125) |
| `startDevServer` | Starts dev server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/index.ts#L15) |
| `startNodeVeryfrontServer` | Starts node veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L575) |
| `startProductionServer` | Starts production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L181) |
Expand All @@ -77,7 +77,7 @@ await server.fetch(new Request("https://example.com/health"));
| `DiscoveryOptions` | Configuration for AI primitives discovery during server startup | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L119) |
| `FileWatcherMetrics` | Public API contract for file watcher metrics. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/types.ts#L33) |
| `GracefulProductionShutdownOptions` | Inputs required to drain and stop a production server process. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/graceful-shutdown.ts#L25) |
| `HostedEnvironmentName` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L46) |
| `HostedEnvironmentName` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L49) |
| `NodeVeryfrontServiceServer` | Public API contract for node veryfront service server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L83) |
| `RouteDirectory` | Public API contract for route directory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/types.ts#L27) |
| `ServerHandle` | Public API contract for server handle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L156) |
Expand Down
2 changes: 1 addition & 1 deletion src/cache/backends/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function isApiCacheAvailable(): boolean {

const isProduction = proxyMode === "1" ||
nodeEnv === "production" ||
!!(apiUrl && !apiUrl.includes("localhost") && !apiUrl.includes("lvh.me"));
!!(apiUrl && !apiUrl.includes("localhost"));

return isProduction && !!apiUrl;
}
Expand Down
8 changes: 3 additions & 5 deletions src/proxy/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1662,11 +1662,9 @@ describe("Proxy Handler", () => {

for (
const host of [
"lvh.me",
"localhost",
"veryfront.dev",
"preview.lvh.me",
"staging.lvh.me",
"preview.localhost",
"staging.localhost",
]
) {
const ctx = await handler.processRequest(
Expand Down Expand Up @@ -2085,7 +2083,7 @@ describe("Proxy Handler", () => {
},
});

const req = new Request("http://my-project.preview.lvh.me:3001/page");
const req = new Request("http://my-project.preview.localhost:3001/page");

const ctx = await handler.processRequest(req);

Expand Down
2 changes: 1 addition & 1 deletion src/proxy/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
// configuration gap reported as an upstream failure. A custom domain in
// that state already answers 404.
//
// Locally the same shape means something else: on lvh.me and friends a
// Locally the same shape means something else: on localhost a
// project-less host is how the project chooser is reached, so those keep
// forwarding. See ProjectsHandler, enabled for exactly this state.
if (isHostedVeryfrontDomain(host)) {
Expand Down
4 changes: 2 additions & 2 deletions src/proxy/mode-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe("Proxy-Renderer Mode Parity", () => {
environment: "preview",
contentSourceId: "local-main",
localPath: "/Users/dev/projects/local-project",
host: "local-project.lvh.me:8080",
host: "local-project.localhost:8080",
parsedDomain: {
slug: "local-project",
isVeryfrontDomain: true,
Expand All @@ -123,7 +123,7 @@ describe("Proxy-Renderer Mode Parity", () => {
};

const injected = injectContextHeaders(
new Request("http://local-project.lvh.me:8080/page"),
new Request("http://local-project.localhost:8080/page"),
ctx,
);

Expand Down
2 changes: 1 addition & 1 deletion src/security/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ the trusted shell.

When Fetch Metadata is present, only `sec-fetch-site: none` (address bar or
bookmark navigation) and `same-origin` requests pass. A link from a rendered
project site such as `project.lvh.me:3000` to `lvh.me:3000/_dev` is same-site
project site such as `project.localhost:3000` to `localhost:3000/_dev` is same-site
but cross-origin and is rejected with `403` by design: sibling local origins
execute untrusted project code and must not be able to drive any privileged
local control. Open the dashboard directly instead; this is intended behavior,
Expand Down
25 changes: 10 additions & 15 deletions src/security/http/local-control-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,6 @@ describe("local control request admission", () => {
"http://[::ffff:7f00:1]:3000/_dev",
"http://project.localhost:3000/_dev",
"http://project.preview.localhost:3000/_dev",
"http://lvh.me:3000/_dev",
"http://project.lvh.me:3000/_dev",
"http://project.preview.lvh.me:3000/_dev",
]
) {
const parsed = new URL(url);
Expand All @@ -136,19 +133,11 @@ describe("local control request admission", () => {

for (
const url of [
"http://lvh.me.attacker.example:3000/_dev",
"http://veryfront.dev:3000/_dev",
"http://project.veryfront.dev:3000/_dev",
"http://production.lvh.me:3000/_dev",
"http://project.production.lvh.me:3000/_dev",
"http://project.staging.lvh.me:3000/_dev",
"http://example.com.prod.lvh.me:3000/_dev",
"http://project.unknown.lvh.me:3000/_dev",
// `localhost` is a single label, so it has no registrable domain to key
// the shape check on. It still gets the same shape check as the
// two-label roots: moving the printed dev hostname onto it must not
// promote production, staging, custom-domain simulation, unknown
// namespaces, or arbitrarily deep names into control authorities.
// the shape check on. It still gets the same shape check the two-label
// roots got: the printed dev hostname living on it must not promote
// production, staging, custom-domain simulation, unknown namespaces, or
// arbitrarily deep names into control authorities.
"http://production.localhost:3000/_dev",
"http://staging.localhost:3000/_dev",
"http://project.production.localhost:3000/_dev",
Expand All @@ -158,6 +147,12 @@ describe("local control request admission", () => {
"http://a.b.c.localhost:3000/_dev",
"http://localhost.attacker.example:3000/_dev",
"http://attacker.example:3000/_dev",
// Public wildcard-DNS roots that resolve to 127.0.0.1 are ordinary
// registrable domains, never control authorities. `localhost` is the
// only named root; anything else is denied on the name alone.
"http://wildcard-dns.example:3000/_dev",
"http://project.wildcard-dns.example:3000/_dev",
"http://project.preview.wildcard-dns.example:3000/_dev",
]
) {
const parsed = new URL(url);
Expand Down
39 changes: 18 additions & 21 deletions src/security/http/local-control-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,21 @@ function isCanonicalDnsHostname(hostname: string): boolean {
}

/**
* Trusted local-control roots, longest-suffix-first.
* The single trusted local-control root.
*
* `localhost` is a single label and therefore has no registrable domain in the
* eTLD+1 sense, so the shape check cannot be expressed as "keep the last two
* labels". Each root is matched as a whole suffix instead and the labels in
* labels". The root is matched as a whole suffix instead and the labels in
* front of it are what the shape rules below constrain.
*/
const TRUSTED_LOCAL_CONTROL_ROOTS = Object.freeze(["localhost", "lvh.me"] as const);
const TRUSTED_LOCAL_CONTROL_ROOT = "localhost";

/** Labels in front of a trusted root, or null when the host is not on one. */
/** Labels in front of the trusted root, or null when the host is not on it. */
function localControlSubLabels(hostname: string): string[] | null {
for (const root of TRUSTED_LOCAL_CONTROL_ROOTS) {
if (hostname === root) return [];
const suffix = `.${root}`;
if (hostname.endsWith(suffix)) {
return hostname.slice(0, -suffix.length).split(".");
}
if (hostname === TRUSTED_LOCAL_CONTROL_ROOT) return [];
const suffix = `.${TRUSTED_LOCAL_CONTROL_ROOT}`;
if (hostname.endsWith(suffix)) {
return hostname.slice(0, -suffix.length).split(".");
}
return null;
}
Expand Down Expand Up @@ -129,19 +127,18 @@ function hasTrustedFetchSite(request: Request): boolean {
* Dedicated authority allowlist for privileged local controls.
*
* `localhost` is reserved by RFC 6761, never leaves the machine, and is the
* hostname printed by the local CLI. Veryfront admits `lvh.me` because the
* documented local-development workflow reaches projects through it; the hostname alone never grants
* access because `isTrustedLocalControlRequest` still requires an
* authenticated loopback transport peer and no proxy hop. Other third-party
* wildcard DNS and development test domains are not control authorities even
* when normal application routing accepts them. Named roots admit only the
* bare host, one project label, or one project below `preview`; production,
* staging, custom-domain simulation, and unknown namespaces stay denied.
* hostname printed by the local CLI. It is the only named root; the hostname
* alone never grants access because `isTrustedLocalControlRequest` still
* requires an authenticated loopback transport peer and no proxy hop.
* Third-party wildcard DNS and public development domains that resolve to
* 127.0.0.1 are not control authorities even when normal application routing
* accepts them. The named root admits only the bare host,
* one project label, or one project below `preview`; production, staging,
* custom-domain simulation, and unknown namespaces stay denied.
*
* `*.localhost` gets that same shape check rather than a blanket allow. Trust
* must not widen just because the printed dev hostname became a single-label
* root: `project.production.localhost` and `a.b.c.localhost` are denied exactly
* as `project.production.lvh.me` is.
* must not widen just because the printed dev hostname is a single-label root:
* `project.production.localhost` and `a.b.c.localhost` are denied.
*/
export function isTrustedLocalControlHostname(hostname: string): boolean {
const address = hostname.startsWith("[") && hostname.endsWith("]")
Expand Down
4 changes: 2 additions & 2 deletions src/security/http/response/security-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ export function generateNonce(): string {
* are intentionally excluded because tenant project domains
* (`{slug}.preview.veryfront.com`, etc.) live under the same suffix and
* would otherwise be allowed to iframe each other (tenant-vs-tenant
* clickjacking). Dev hosts (`veryfront.dev`) are omitted because dev mode
* skips the default CSP entirely.
* clickjacking). Local development hosts (`*.localhost`) are omitted because
* dev mode skips the default CSP entirely.
*/
const VERYFRONT_FRAME_ANCESTORS = ["'self'", ...HOSTED_STUDIO_ORIGINS];

Expand Down
2 changes: 0 additions & 2 deletions src/security/http/studio-origin-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ describe("security/http/studio-origin-policy", () => {
// studio.* subdomains are not deployed and are no longer trusted origins.
assertEquals(resolveTrustedStudioOrigin("https://studio.veryfront.com"), null);
assertEquals(resolveTrustedStudioOrigin("https://studio.veryfront.org"), null);
assertEquals(resolveTrustedStudioOrigin("https://studio.veryfront.dev"), null);
assertEquals(resolveTrustedStudioOrigin("http://studio.veryfront.com"), null);
assertEquals(resolveTrustedStudioOrigin("https://studio.veryfront.com:8443"), null);
});
Expand All @@ -35,7 +34,6 @@ describe("security/http/studio-origin-policy", () => {
assertEquals(source.includes('"https://veryfront.com"'), true);
assertEquals(source.includes('"https://studio.veryfront.com"'), false);
assertEquals(source.includes("endsWith"), false);
assertEquals(source.includes(".veryfront.dev"), false);

const resolveTarget = new Function(
"document",
Expand Down
2 changes: 1 addition & 1 deletion src/security/sandbox/worker-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe("worker-script serializeError", () => {

it("does not trust RFC 9457 fields attached to a plain project error", () => {
const err = Object.assign(new Error("not found"), {
type: "https://veryfront.dev/errors/not-found",
type: "https://veryfront.com/errors/not-found",
status: 404,
detail: "Resource was not located",
});
Expand Down
Loading