Skip to content

Commit cf54f93

Browse files
Add token migration and DNS compliance fixes
- Migrate old tokens with underscores to DNS-compliant format - Document entropy reduction from base64url to ~base36 - Ensure tokens don't start/end with hyphens per RFC 952/1123 - Add comprehensive DNS hostname validation in tests - Update changeset to document breaking change Co-authored-by: whoiskatrin <whoiskatrin@users.noreply.github.com>
1 parent 44631d4 commit cf54f93

File tree

3 files changed

+35
-1
lines changed

3 files changed

+35
-1
lines changed

.changeset/fix-preview-url-underscores.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
Fix preview URL DNS compliance by removing underscores from tokens
66

77
Preview URLs generated by `exposePort()` now use only DNS-valid characters (RFC 952/1123). The token generation now replaces both `+` and `/` characters with hyphens instead of using underscores, ensuring all generated hostnames are valid for DNS resolution.
8+
9+
**Breaking change:** Existing preview URL tokens stored in Durable Objects will be automatically regenerated on next access. Old preview URLs containing underscores will stop working after this update. Users will need to call `exposePort()` again to get new DNS-compliant preview URLs.

packages/sandbox/src/sandbox.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
191191
this.portTokens.set(parseInt(portStr, 10), token);
192192
}
193193

194+
// Migrate old tokens with underscores to DNS-compliant format
195+
// Old tokens used underscores which violate RFC 952/1123 DNS hostname requirements
196+
let needsMigration = false;
197+
for (const [port, token] of this.portTokens.entries()) {
198+
if (token.includes('_')) {
199+
this.portTokens.set(port, this.generatePortToken());
200+
needsMigration = true;
201+
}
202+
}
203+
if (needsMigration) {
204+
await this.persistPortTokens();
205+
}
206+
194207
// Load saved timeout configuration (highest priority)
195208
const storedTimeouts =
196209
await this.ctx.storage.get<
@@ -1656,12 +1669,27 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
16561669

16571670
// Convert to base64url format (URL-safe, no padding, lowercase)
16581671
// Use hyphen for both + and / to ensure DNS hostname compatibility (RFC 952/1123)
1672+
// Note: This reduces the effective character set from 64 (base64url) to ~36 chars,
1673+
// reducing entropy from 96 bits to ~82 bits. Still cryptographically strong for token security.
16591674
const base64 = btoa(String.fromCharCode(...array));
1660-
return base64
1675+
let token = base64
16611676
.replace(/\+/g, '-')
16621677
.replace(/\//g, '-')
16631678
.replace(/=/g, '')
16641679
.toLowerCase();
1680+
1681+
// Ensure token doesn't end with hyphen (RFC 952/1123 requirement)
1682+
// Replace trailing hyphens with alphanumeric chars from the token
1683+
while (token.endsWith('-')) {
1684+
token = token.slice(0, -1) + token.charAt(Math.floor(Math.random() * (token.length - 1)));
1685+
}
1686+
1687+
// Ensure token doesn't start with hyphen
1688+
while (token.startsWith('-')) {
1689+
token = token.charAt(Math.floor(Math.random() * (token.length - 1))) + token.slice(1);
1690+
}
1691+
1692+
return token;
16651693
}
16661694

16671695
private async persistPortTokens(): Promise<void> {

packages/sandbox/tests/sandbox.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,10 @@ describe('Sandbox - Automatic Session Management', () => {
877877
const url = result.url;
878878
const hostname = new URL(url).hostname;
879879

880+
// Validate full hostname RFC 952/1123 compliance
881+
// Labels cannot start or end with hyphens
882+
expect(hostname).toMatch(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/);
883+
880884
// Extract token from hostname pattern: port-sandboxId-token.domain
881885
const match = hostname.match(/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9-]{16})\.(.+)$/);
882886
expect(match).toBeTruthy();

0 commit comments

Comments
 (0)