Skip to content

Commit b395162

Browse files
author
Nils Bars
committed
Warn about vite dev exposure and reflect the course in the page title
SPA: - DefaultLayout renders a yellow/black hazard-striped v-system-bar at the top of every page when import.meta.env.DEV is true, with the text "Served by vite dev with HMR — do not expose this instance publicly." The whole block is tree-shaken out of the production vite build. - App.vue sets document.title from the nav store's courseName ("REF - <course>"), falling back to plain "REF" until hydrate finishes or if the API call fails. Docs: - README gains "Single-port web interface" and "Hot reloading (local development only)" sections spelling out that --hot-reloading runs vite dev behind Caddy and must never be used on a publicly reachable host, and that SPA source changes require ./ctrl.sh build in prod mode since the bundle is baked into the frontend-proxy image. - docs/ARCHITECTURE.md adds /admin and /spa redirect notes, a Dev-mode security warning block covering the vite dev attack surface, a note on ctrl.sh's COMPOSE_PROFILES=dev wiring, and a warning box next to the --hot-reloading command reference. - .claude/CLAUDE.md splits the web description into separate entries for the Flask webapp (internal port only) and the Caddy frontend-proxy, with the full routing table, profile gate, and security warning.
1 parent 8a2ccc4 commit b395162

5 files changed

Lines changed: 140 additions & 7 deletions

File tree

.claude/CLAUDE.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,35 @@ REF is a containerized platform for hosting programming exercises with isolated
155155

156156
### Components
157157

158-
1. **Web Application** (`webapp/`) - Flask app on port 8000
158+
1. **Web Application** (`webapp/`) - Flask app served by uWSGI on internal port 8000 (not published; reached via `frontend-proxy`)
159159
- `ref/view/` - HTML route handlers (exercises, grading, instances, file browser, visualization, admin student management, system settings, etc.)
160160
- `ref/services_api/` - JSON endpoints called by services (SSH reverse proxy hooks in `ssh.py`, student container callbacks in `instance.py`)
161161
- `ref/frontend_api/` - JSON endpoints consumed by the Vue SPA (registration/restore-key in `students.py`, public scoreboard in `scoreboard.py`; mounted under `/api/v2/*` + `/api/scoreboard/*`)
162162
- `ref/model/` - SQLAlchemy models (users, groups, exercises, instances, submissions, grades, system settings)
163163
- `ref/core/` - Business logic managers (`ExerciseManager`, `InstanceManager`, `ExerciseImageManager`, `UserManager`, `DockerClient`, etc.)
164164

165-
Student-facing pages (registration, restore-key, public scoreboard) are served by the Vue SPA under `/spa/*` and talk to `ref/frontend_api/`. Admin pages live under `ref/view/` as Jinja-rendered HTML. The Caddy `frontend-proxy` container fronts both on a single host port 8000 — it reverse-proxies `/spa/*` to `spa-frontend:5173` (dev, with HMR) or serves a baked SPA bundle (prod), serves Flask's `/static/*` directly, and proxies everything else to `web:8000`.
165+
Student-facing pages (registration, restore-key, public scoreboard) are served by the Vue SPA under `/spa/*` and talk to `ref/frontend_api/`. Admin pages live under `ref/view/` as Jinja-rendered HTML.
166166

167-
2. **SSH Reverse Proxy** (`ssh-reverse-proxy/`) - Rust-based SSH proxy on port 2222
167+
2. **Frontend Proxy** (`frontend-proxy/`) - Caddy 2 container that fronts the Flask `web` service and the Vue SPA on a **single host port** (`HTTP_HOST_PORT`, default 8000). Multi-stage Dockerfile: stage 1 builds the SPA with Node; stage 2 is `caddy:2-alpine` with `dist/` baked in at `/srv/spa-dist`. Routes:
168+
- `/spa/*``spa-frontend:5173` (dev) or baked `/srv/spa-dist` via `file_server` (prod)
169+
- `/static/*` → bind-mount of `webapp/ref/static` served directly
170+
- `/admin`, `/admin/` → 302 to `/admin/exercise/view`
171+
- `/spa` → 308 `/spa/`
172+
- everything else → `reverse_proxy web:8000` with `header_up X-Tinyproxy {remote_host}` so Flask's rate limiter keys on the real client IP
173+
Dev/prod is selected at container start by `entrypoint.sh` via `$HOT_RELOADING`. The `spa-frontend` service is gated behind the `dev` compose profile, and `ctrl.sh` exports `COMPOSE_PROFILES=dev` when `--hot-reloading` is active. **Never run `--hot-reloading` on a publicly reachable host**`vite dev` is not a production server. The SPA renders a hazard-striped warning banner when `import.meta.env.DEV` is true.
174+
175+
3. **SSH Reverse Proxy** (`ssh-reverse-proxy/`) - Rust-based SSH proxy on port 2222
168176
- Routes student SSH connections to exercise containers
169177
- Uses web API with HMAC-signed requests for authentication and provisioning
170178
- Supports shell, exec, SFTP, local/remote port forwarding, and X11 forwarding
171179

172-
3. **Instance Container** (`ref-docker-base/`) - Ubuntu 24.04 with dev tools
180+
4. **Instance Container** (`ref-docker-base/`) - Ubuntu 24.04 with dev tools
173181
- Isolated per student/exercise under `ref-instances.slice` cgroup
174182
- SSH server on port 13370
175183
- Contains `ref-utils` for submission testing
176184
- `task`/`_task` scripts for submission testing, `reset-env` for container reset
177185

178-
4. **Database** - PostgreSQL 17.2 storing users, groups, exercises, instances, submissions, grades, system settings
186+
5. **Database** - PostgreSQL 17.2 storing users, groups, exercises, instances, submissions, grades, system settings
179187

180188
### Connection Flow
181189

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,38 @@ After successfully building REF, the database has to be initialized:
4444
./ctrl.sh down
4545
```
4646

47+
### Single-port web interface
48+
49+
The Flask web app (`/`, `/admin/*`, `/api/*`), the Vue student SPA
50+
(`/spa/*`), and Flask's static assets (`/static/*`) are all fronted by
51+
a single Caddy reverse proxy (`frontend-proxy/`) on host port 8000.
52+
Students go to `http://<host>:8000/spa/register` (or whichever landing
53+
page is configured); admins go to `http://<host>:8000/admin/` (which
54+
redirects to the exercise view). The SSH entry point for student
55+
containers is unchanged on port 2222.
56+
57+
In production, `frontend-proxy` serves the Vue SPA as a static bundle
58+
that is baked into the image at `docker build` time via a multi-stage
59+
Dockerfile — rebuild with `./ctrl.sh build` after any change under
60+
`spa-frontend/`.
61+
62+
### Hot reloading (local development only)
63+
64+
Use `./ctrl.sh up --hot-reloading` during local development to get
65+
Flask auto-reload **and** Vite HMR for the SPA. This flag starts an
66+
extra `spa-frontend` container (running `vite dev`) which is gated
67+
behind the `dev` compose profile; Caddy then reverse-proxies `/spa/*`
68+
(including the HMR websocket) to that container.
69+
70+
> ⚠️ **Do not run `--hot-reloading` on a publicly reachable host.**
71+
> Vite's dev server is not designed for hostile clients — it serves
72+
> raw source, exposes `/@fs/`, and has had several path-traversal CVEs
73+
> in recent releases. While `--hot-reloading` is active, the SPA
74+
> renders a yellow/black hazard-striped warning strip at the top of
75+
> every page as a visible reminder; the whole warning is tree-shaken
76+
> out of the production `vite build`, so nothing about it leaks into
77+
> the prod bundle. See `docs/ARCHITECTURE.md` for the full model.
78+
4779

4880
#### Build the custom Linux kernel
4981
Building the custom Linux kernel is only required if you need the `no-randomize` attribute for some exercises. This attribute allows you to disable ASLR for a specific binary, even if it is a setuid binary. This is not allowed for unmodified kernels. The following assumes that your system is based on Debian and uses GRUB as a bootloader. For other systems or bootloaders, the instructions have to be adapted accordingly.

docs/ARCHITECTURE.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,31 @@ fallback) based on `$HOT_RELOADING`.
142142
- SPA hashed assets (`/spa/assets/*`) are served with
143143
`public, max-age=31536000, immutable`; `index.html` is `no-cache` so
144144
deploys are picked up atomically.
145+
- `/admin` and `/admin/` 302-redirect to `/admin/exercise/view`.
146+
- `/spa` 308-redirects to `/spa/` so bare URLs get a trailing slash.
147+
148+
**Dev-mode security warning:**
149+
150+
`./ctrl.sh up --hot-reloading` is a **local-development-only** flag.
151+
When it is set:
152+
153+
1. The `spa-frontend` container is started (gated behind the `dev`
154+
compose profile) and runs `vite dev` with HMR.
155+
2. `frontend-proxy` selects `Caddyfile.dev` and reverse-proxies
156+
`/spa/*` (including the HMR websocket) to `spa-frontend:5173`
157+
without any auth or IP filter.
158+
3. `vite dev` serves raw, unbundled source files and exposes a
159+
`/@fs/` endpoint. Vite has had several path-traversal CVEs against
160+
`/@fs/` in recent releases (CVE-2025-30208/31125/31486/32395/46565);
161+
even on a patched version, the dev server is not designed for
162+
hostile clients.
163+
164+
**Never run a publicly-reachable REF instance with `--hot-reloading`.**
165+
To make this obvious in the UI, the SPA `DefaultLayout` renders a
166+
hazard-striped warning strip at the very top of every page when
167+
`import.meta.env.DEV` is true; this block is tree-shaken out of the
168+
production `vite build` entirely, so only dev-mode clients ever see
169+
it and the prod bundle contains no trace of the warning code.
145170

146171
### 3. SSH Reverse Proxy (`ssh-reverse-proxy/`)
147172

@@ -227,7 +252,7 @@ exercises/<name>/
227252
./ctrl.sh build # Build Docker images
228253
./ctrl.sh up [--debug] # Start services (--debug attaches with logs)
229254
./ctrl.sh up --maintenance # Start in maintenance mode
230-
./ctrl.sh up --hot-reloading # Start with hot reloading
255+
./ctrl.sh up --hot-reloading # Start with hot reloading (LOCAL DEV ONLY; see warning below)
231256
./ctrl.sh down # Stop and remove services
232257
./ctrl.sh stop # Stop without removing
233258
./ctrl.sh restart # Restart all services
@@ -240,6 +265,22 @@ exercises/<name>/
240265

241266
Pre-flight checks: submodule validation, Docker/cgroup v2 requirements, configuration validation.
242267

268+
When `--hot-reloading` is passed, `ctrl.sh` exports `COMPOSE_PROFILES=dev`
269+
for every compose subcommand. This activates the `dev` compose profile
270+
which is the gate on the `spa-frontend` service — without it, `vite dev`
271+
is not started at all. In prod mode, `ctrl.sh up` unsets
272+
`COMPOSE_PROFILES` so `spa-frontend` stays off; the other commands
273+
(`build`, `down`, `stop`, `ps`, `logs`, …) keep the profile active so
274+
profile-gated services can still be built and cleaned up.
275+
276+
> **SECURITY — do not run `--hot-reloading` on a publicly reachable
277+
> host.** The flag starts Vite's dev server behind Caddy with no auth,
278+
> serving raw source over `/spa/*` (including the `/@fs/` endpoint,
279+
> which has had repeated path-traversal CVEs). The SPA itself renders
280+
> a hazard-striped warning strip at the top of every page in this mode
281+
> as a visible reminder. Intended use is local development against
282+
> `http://localhost:8000` only.
283+
243284
## Test Structure
244285

245286
```

spa-frontend/src/App.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
<script setup lang="ts">
2-
import { onMounted } from 'vue';
2+
import { onMounted, watchEffect } from 'vue';
33
import DefaultLayout from './layouts/DefaultLayout.vue';
44
import { useNavStore } from './stores/nav';
55
import { useTheme } from './theme/useTheme';
66
77
const nav = useNavStore();
88
const theme = useTheme();
99
10+
watchEffect(() => {
11+
document.title = nav.courseName === 'REF' ? 'REF' : `REF - ${nav.courseName}`;
12+
});
13+
1014
onMounted(async () => {
1115
theme.init();
1216
await nav.hydrate();

spa-frontend/src/layouts/DefaultLayout.vue

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { useTheme } from '../theme/useTheme';
66
const nav = useNavStore();
77
const theme = useTheme();
88
9+
// import.meta.env.DEV is true when Vite serves the app via `vite dev`
10+
// (HMR); a production `vite build` bakes this to false. The banner is
11+
// tree-shaken out of the prod bundle entirely.
12+
const devMode = import.meta.env.DEV;
13+
914
const themeIcon = computed(() => {
1015
switch (theme.mode.value) {
1116
case 'dark':
@@ -31,6 +36,19 @@ const themeLabel = computed(() => {
3136

3237
<template>
3338
<v-app>
39+
<v-system-bar
40+
v-if="devMode"
41+
class="dev-banner"
42+
height="26"
43+
role="status"
44+
aria-label="Development server warning"
45+
>
46+
<v-icon size="16" class="mr-2">mdi-alert</v-icon>
47+
<span>
48+
Served by <strong>vite dev</strong> with HMR &mdash; do not expose
49+
this instance publicly.
50+
</span>
51+
</v-system-bar>
3452
<v-app-bar color="background" border="b" class="term-appbar">
3553
<template #prepend>
3654
<v-app-bar-title class="term-appbar-title">
@@ -78,3 +96,33 @@ const themeLabel = computed(() => {
7896
</v-main>
7997
</v-app>
8098
</template>
99+
100+
<style scoped>
101+
.dev-banner {
102+
display: flex;
103+
align-items: center;
104+
justify-content: center;
105+
gap: 0.25rem;
106+
padding: 0.35rem 0.75rem;
107+
font-size: 0.8rem;
108+
font-weight: 600;
109+
letter-spacing: 0.02em;
110+
text-transform: uppercase;
111+
color: #1a1a1a;
112+
background: repeating-linear-gradient(
113+
-45deg,
114+
#f9b000,
115+
#f9b000 12px,
116+
#1a1a1a 12px,
117+
#1a1a1a 24px
118+
);
119+
}
120+
.dev-banner > span {
121+
padding: 0.15rem 0.5rem;
122+
background: #f9b000;
123+
border-radius: 2px;
124+
}
125+
.dev-banner strong {
126+
font-weight: 800;
127+
}
128+
</style>

0 commit comments

Comments
 (0)