Skip to content

Commit 30fd35b

Browse files
committed
fix: harden user content and connection handling
1 parent 3155144 commit 30fd35b

19 files changed

Lines changed: 1173 additions & 817 deletions

.env.example

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,12 @@ CERT_CHECK_HOURS=12
6767
# IP_CERT_RENEWAL_HOURS=96
6868

6969
# === Security ===
70-
# Allow all response headers including dangerous ones like Service-Worker-Allowed
71-
# Default: false (blocked on main domain path-based routing /r/)
72-
ALLOW_ALL_HEADERS=false
70+
# Serve user-controlled content at /r/<subdomain> on the dashboard origin.
71+
# This is useful for domainless/IP deployments, but weakens origin isolation.
72+
DANGEROUSLY_ALLOW_SAME_ORIGIN_USER_CONTENT=false
73+
# Allow origin-scoped headers such as Service-Worker-Allowed and Alt-Svc on /r/ routes.
74+
# Has no effect unless same-origin user content is enabled above.
75+
DANGEROUSLY_ALLOW_ALL_HEADERS=false
7376

7477
# === Error Tracking (Sentry) ===
7578
# SENTRY_DSN_BACKEND=https://xxx@sentry.io/xxx

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ See [.env.example](.env.example) for all available options.
189189
| `MAX_REQUEST_BODY_MB` | No | `10` | Max HTTP request body size |
190190
| `SESSION_RATE_LIMIT` | No | `10` | Max sessions per IP per window |
191191
| `SESSION_RATE_WINDOW_SECS` | No | `60` | Rate limit window in seconds |
192-
| `ALLOW_ALL_HEADERS` | No | `false` | Allow dangerous headers (e.g., Service-Worker-Allowed) |
192+
| `DANGEROUSLY_ALLOW_SAME_ORIGIN_USER_CONTENT` | No | `false` | Enable `/r/<subdomain>` user content on the dashboard origin (for domainless/IP deployments) |
193+
| `DANGEROUSLY_ALLOW_ALL_HEADERS` | No | `false` | Allow origin-scoped headers such as `Service-Worker-Allowed` and `Alt-Svc` on `/r/` routes |
193194
| `RUST_LOG` | No | `info` | Log level (trace, debug, info, warn, error) |
194195

195196
## Interface

docker-compose.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ services:
4444
- MAX_SUBDOMAIN_SIZE_MB=${MAX_SUBDOMAIN_SIZE_MB:-10}
4545
- MAX_REQUEST_BODY_MB=${MAX_REQUEST_BODY_MB:-10}
4646
# Optional - Security
47-
- ALLOW_ALL_HEADERS=${ALLOW_ALL_HEADERS:-false}
47+
- DANGEROUSLY_ALLOW_SAME_ORIGIN_USER_CONTENT=${DANGEROUSLY_ALLOW_SAME_ORIGIN_USER_CONTENT:-false}
48+
- DANGEROUSLY_ALLOW_ALL_HEADERS=${DANGEROUSLY_ALLOW_ALL_HEADERS:-false}
4849
# Optional - Error tracking
4950
- SENTRY_DSN_BACKEND=${SENTRY_DSN_BACKEND:-}
5051
- SENTRY_DSN_FRONTEND=${SENTRY_DSN_FRONTEND:-}

frontend/bun.lock

Lines changed: 432 additions & 336 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,44 @@
1111
"test": "echo 'No tests configured yet'"
1212
},
1313
"dependencies": {
14-
"@heroui/react": "^2.8.7",
14+
"@heroui/react": "^2.8.10",
1515
"@monaco-editor/react": "^4.7.0",
16-
"@sentry/react": "^8.0.0",
17-
"@tanstack/react-query": "^5.90.16",
18-
"axios": "^1.13.2",
16+
"@sentry/react": "^8.55.2",
17+
"@tanstack/react-query": "^5.101.4",
18+
"axios": "^1.19.0",
1919
"clsx": "^2.1.1",
20-
"esbuild": "^0.27.2",
20+
"esbuild": "^0.28.2",
2121
"flag-icons": "^7.5.0",
22-
"framer-motion": "^12.26.1",
22+
"framer-motion": "^12.43.0",
2323
"lucide-react": "^0.562.0",
24-
"monaco-editor": "^0.55.1",
25-
"react": "^19.2.0",
26-
"react-dom": "^19.2.0",
27-
"react-router-dom": "^7.12.0",
28-
"sonner": "^2.0.7",
29-
"tailwind-merge": "^3.4.0",
24+
"monaco-editor": "^0.56.0",
25+
"react": "^19.2.8",
26+
"react-dom": "^19.2.8",
27+
"react-router-dom": "^7.18.2",
28+
"sonner": "^2.0.8",
29+
"tailwind-merge": "^3.6.0",
3030
"vite-plugin-monaco-editor": "^1.1.0",
31-
"zustand": "^5.0.10"
31+
"zustand": "^5.0.15"
3232
},
3333
"devDependencies": {
34-
"@eslint/js": "^9.39.1",
35-
"@tanstack/react-query-devtools": "^5.91.2",
36-
"@types/node": "^24.10.1",
37-
"@types/react": "^19.2.5",
38-
"@types/react-dom": "^19.2.3",
39-
"@vitejs/plugin-react": "^5.1.1",
40-
"autoprefixer": "^10.4.23",
41-
"eslint": "^9.39.1",
42-
"eslint-plugin-react-hooks": "^7.0.1",
43-
"eslint-plugin-react-refresh": "^0.4.24",
34+
"@eslint/js": "^9.39.5",
35+
"@tanstack/react-query-devtools": "^5.101.4",
36+
"@types/node": "^24.13.3",
37+
"@types/react": "^19.2.18",
38+
"@types/react-dom": "^19.2.4",
39+
"@vitejs/plugin-react": "^5.2.0",
40+
"autoprefixer": "^10.5.4",
41+
"eslint": "^9.39.5",
42+
"eslint-plugin-react-hooks": "^7.1.1",
43+
"eslint-plugin-react-refresh": "^0.4.26",
4444
"globals": "^16.5.0",
45-
"postcss": "^8.5.6",
45+
"postcss": "^8.5.26",
4646
"tailwindcss": "3.4.17",
4747
"typescript": "~5.9.3",
48-
"typescript-eslint": "^8.46.4",
49-
"vite": "^7.2.4"
48+
"typescript-eslint": "^8.67.0",
49+
"vite": "^7.3.6"
50+
},
51+
"overrides": {
52+
"dompurify": "3.4.14"
5053
}
5154
}

frontend/src/api/client.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ const api = axios.create({
1616
withCredentials: true, // Send cookies with requests (for admin_token)
1717
});
1818

19+
const authHeaders = (token: string) => ({
20+
Authorization: `Bearer ${token}`,
21+
});
22+
1923
// Helper to check if we're in offline mode
2024
function isNetworkError(error: unknown): boolean {
2125
if (error instanceof AxiosError) {
@@ -51,7 +55,7 @@ export async function createSession(
5155
export async function getDnsRecords(token: string): Promise<DnsRecord[]> {
5256
try {
5357
const { data } = await api.get<{ records: DnsRecord[] }>("/dns", {
54-
params: { token },
58+
headers: authHeaders(token),
5559
});
5660
return data.records || [];
5761
} catch (error) {
@@ -67,14 +71,14 @@ export async function updateDnsRecords(
6771
token: string,
6872
records: DnsRecord[],
6973
): Promise<void> {
70-
await api.put("/dns", { records }, { params: { token } });
74+
await api.put("/dns", { records }, { headers: authHeaders(token) });
7175
}
7276

7377
// Files API
7478
export async function getFiles(token: string): Promise<FileTree> {
7579
try {
7680
const { data } = await api.get<FileTree>("/files", {
77-
params: { token },
81+
headers: authHeaders(token),
7882
});
7983
return data;
8084
} catch (error) {
@@ -96,7 +100,7 @@ export async function updateFiles(
96100
token: string,
97101
files: FileTree,
98102
): Promise<void> {
99-
await api.put("/files", files, { params: { token } });
103+
await api.put("/files", files, { headers: authHeaders(token) });
100104
}
101105

102106
// Requests API
@@ -107,7 +111,8 @@ export async function getRequests(
107111
): Promise<PaginatedResponse<Request>> {
108112
try {
109113
const { data } = await api.get<PaginatedResponse<Request>>("/requests", {
110-
params: { token, limit, offset },
114+
headers: authHeaders(token),
115+
params: { limit, offset },
111116
});
112117
return data;
113118
} catch (error) {
@@ -127,7 +132,7 @@ export async function getRequest(
127132
requestId: string,
128133
): Promise<Request> {
129134
const { data } = await api.get<Request>(`/requests/${requestId}`, {
130-
params: { token },
135+
headers: authHeaders(token),
131136
});
132137
return data;
133138
}
@@ -137,13 +142,13 @@ export async function deleteRequest(
137142
requestId: string,
138143
): Promise<void> {
139144
await api.delete(`/requests/${requestId}`, {
140-
params: { token },
145+
headers: authHeaders(token),
141146
});
142147
}
143148

144149
export async function deleteAllRequests(token: string): Promise<void> {
145150
await api.delete("/requests", {
146-
params: { token },
151+
headers: authHeaders(token),
147152
});
148153
}
149154

@@ -155,7 +160,7 @@ export async function createShareToken(
155160
const { data } = await api.post<{ share_token: string }>(
156161
`/requests/${requestId}/share`,
157162
{},
158-
{ params: { token } },
163+
{ headers: authHeaders(token) },
159164
);
160165
return data.share_token;
161166
}

frontend/src/main.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ declare global {
2727
__CONFIG__?: {
2828
DOMAIN?: string;
2929
SENTRY_DSN_FRONTEND?: string;
30+
DANGEROUSLY_ALLOW_SAME_ORIGIN_USER_CONTENT?: boolean;
3031
};
3132
}
3233
}
@@ -36,6 +37,11 @@ const scrubSensitiveParams = (url: string): string => {
3637
const u = new URL(url, window.location.origin);
3738
u.searchParams.delete("token");
3839
u.searchParams.delete("share");
40+
u.searchParams.delete("request");
41+
u.pathname = u.pathname.replace(
42+
/\/api\/v2\/requests\/shared\/[^/]+$/,
43+
"/api/v2/requests/shared/<redacted>",
44+
);
3945
return u.toString();
4046
} catch {
4147
return url;

frontend/src/pages/RequestsPage.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export function RequestsPage() {
8484
const subdomain = session?.subdomain || "xxxxxxxx";
8585
const fullHttpDomain = `${subdomain}.${httpDomain}`;
8686
const fullDnsDomain = `${subdomain}.${dnsDomain}`;
87+
const allowSameOriginUserContent =
88+
window.__CONFIG__?.DANGEROUSLY_ALLOW_SAME_ORIGIN_USER_CONTENT === true;
8789

8890
// Mark as visited when selected (only for non-shared requests)
8991
useEffect(() => {
@@ -134,9 +136,11 @@ export function RequestsPage() {
134136

135137
<div className="space-y-2 font-mono text-sm">
136138
<Code className="block p-2">curl http://{fullHttpDomain}</Code>
137-
<Code className="block p-2">
138-
curl http://{httpDomain}/r/{subdomain}/
139-
</Code>
139+
{allowSameOriginUserContent && (
140+
<Code className="block p-2">
141+
curl http://{httpDomain}/r/{subdomain}/
142+
</Code>
143+
)}
140144
<Code className="block p-2">
141145
curl -X POST --data hello http://{fullHttpDomain}
142146
</Code>

0 commit comments

Comments
 (0)