Skip to content

Commit 004ee89

Browse files
committed
security: harden API key handling and add startup warnings
- Increase auto-generated API key entropy from 192-bit to 384-bit - Use hmac.compare_digest() for constant-time key comparison (HTTP + WS) - Refuse to start without OPEN_TERMINAL_API_KEY configured - Add prominent CORS wildcard warning at startup - Bump version to 0.11.30
1 parent 952c3f1 commit 004ee89

4 files changed

Lines changed: 32 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [0.11.30] - 2026-03-25
8+
9+
### Changed
10+
11+
- 🔑 **Stronger auto-generated API keys** — increased from 192-bit (32 chars) to 384-bit (64 chars) entropy, making brute-force attacks computationally infeasible.
12+
- 🔒 **API key required to start** — the server now refuses to start without an API key configured. The CLI auto-generates one as before, but running via `uvicorn` directly without setting `OPEN_TERMINAL_API_KEY` is no longer allowed.
13+
- 🛡️ **Constant-time API key comparison** — both HTTP and WebSocket authentication now use `hmac.compare_digest()` instead of `!=`, preventing timing-based key extraction.
14+
- ⚠️ **CORS default warning** — a prominent yellow box is printed at startup when `--cors-allowed-origins` is left at the default `*`, warning operators to restrict it for production.
15+
716
## [0.11.29] - 2026-03-24
817

918
### Fixed

open_terminal/cli.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def run(
8686

8787
generated = not api_key
8888
if not api_key:
89-
api_key = secrets.token_urlsafe(24)
89+
api_key = secrets.token_urlsafe(48)
9090

9191
os.environ["OPEN_TERMINAL_API_KEY"] = api_key
9292
os.environ["OPEN_TERMINAL_CORS_ALLOWED_ORIGINS"] = cors_allowed_origins
@@ -118,6 +118,16 @@ def run(
118118
click.echo(click.style(" Use --host 127.0.0.1 to restrict to this machine.", dim=True))
119119
click.echo()
120120

121+
if cors_allowed_origins.strip() == "*":
122+
click.echo(click.style(" ┌─────────────────────────────────────────────────────────────┐", fg="yellow"))
123+
click.echo(click.style(" │ ⚠ CORS is set to '*' (allow all origins) │", fg="yellow"))
124+
click.echo(click.style(" │ │", fg="yellow"))
125+
click.echo(click.style(" │ Any website can make requests to this server. │", fg="yellow"))
126+
click.echo(click.style(" │ For production, restrict with: │", fg="yellow"))
127+
click.echo(click.style(" │ --cors-allowed-origins https://your-domain.com │", fg="yellow"))
128+
click.echo(click.style(" └─────────────────────────────────────────────────────────────┘", fg="yellow"))
129+
click.echo()
130+
121131
from open_terminal.env import UVICORN_LOOP
122132
uvicorn.run("open_terminal.main:app", host=host, port=port, loop=UVICORN_LOOP)
123133

open_terminal/main.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import hmac
23
from importlib.metadata import version as _pkg_version
34
import fnmatch
45
import json
@@ -31,6 +32,14 @@
3132
from open_terminal.utils.user_isolation import check_environment, resolve_user
3233
check_environment()
3334

35+
if not API_KEY:
36+
raise SystemExit(
37+
"\n\033[91m"
38+
" OPEN_TERMINAL_API_KEY is required.\n"
39+
" Set via environment variable or --api-key flag.\n"
40+
"\033[0m"
41+
)
42+
3443
try:
3544
import fcntl
3645
import pty
@@ -94,7 +103,7 @@ async def verify_api_key(
94103
):
95104
if not API_KEY:
96105
return
97-
if not credentials or credentials.credentials != API_KEY:
106+
if not credentials or not hmac.compare_digest(credentials.credentials, API_KEY):
98107
raise HTTPException(status_code=401, detail="Invalid API key")
99108

100109

@@ -1579,7 +1588,7 @@ async def ws_terminal(ws: WebSocket, session_id: str):
15791588
try:
15801589
msg = await asyncio.wait_for(ws.receive_text(), timeout=10.0)
15811590
payload = json.loads(msg)
1582-
if payload.get("type") != "auth" or payload.get("token") != API_KEY:
1591+
if payload.get("type") != "auth" or not hmac.compare_digest(payload.get("token", ""), API_KEY):
15831592
await ws.close(code=4001, reason="Invalid API key")
15841593
return
15851594
except (asyncio.TimeoutError, json.JSONDecodeError, Exception):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "open-terminal"
3-
version = "0.11.29"
3+
version = "0.11.30"
44
description = "A remote terminal API."
55
readme = "README.md"
66
authors = [

0 commit comments

Comments
 (0)