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
1 change: 1 addition & 0 deletions deno.lock

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

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