Enterprise security guide for browser-based terminals. Covers JWT authentication, WebSocket security, session management, container isolation, and OWASP best practices.
Why Terminal Security Matters
Browser-based terminals are powerful—and dangerous if not secured properly. A compromised terminal gives attackers direct shell access to your infrastructure.
In 2025, a critical vulnerability (CVE-2025-52882) in a popular IDE extension allowed malicious websites to connect to unauthenticated local WebSocket servers, enabling remote command execution.
Lesson learned: Always authenticate WebSocket connections, even for local servers.
Part 1: JWT Authentication for Terminal Sessions
Token Structure
1// JWT payload for terminal sessions2interface TerminalTokenPayload {3 sub: string; // User ID4 email: string; // User email5 workspaceId: string; // Target workspace6 permissions: string[]; // e.g., ['terminal:read', 'terminal:write']7 iat: number; // Issued at8 exp: number; // Expiration9 jti: string; // Unique token ID (for revocation)10}Token Generation
1// src/auth/token.ts2import { SignJWT, jwtVerify } from 'jose';3import { randomUUID } from 'crypto';45const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);6const ISSUER = 'terminal-service';7const AUDIENCE = 'terminal-client';89export async function generateTerminalToken(10 userId: string,11 email: string,12 workspaceId: string13): Promise<{ token: string; expiresAt: Date }> {14 const expiresAt = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes1516 const token = await new SignJWT({17 sub: userId,18 email,19 workspaceId,20 permissions: ['terminal:read', 'terminal:write'],21 jti: randomUUID(),22 })23 .setProtectedHeader({ alg: 'HS256' })24 .setIssuedAt()25 .setIssuer(ISSUER)26 .setAudience(AUDIENCE)27 .setExpirationTime(expiresAt)28 .sign(SECRET);2930 return { token, expiresAt };31}3233export async function verifyTerminalToken(34 token: string35): Promise<TerminalTokenPayload> {36 const { payload } = await jwtVerify(token, SECRET, {37 issuer: ISSUER,38 audience: AUDIENCE,39 });4041 return payload as TerminalTokenPayload;42}Token Refresh Flow
Part 2: WebSocket Security
OWASP WebSocket Security Checklist
| Requirement | Implementation |
|---|---|
| Encryption | WSS only (TLS 1.3) |
| Origin validation | Check Origin header |
| Authentication | Validate JWT on connect |
| Authorization | Check permissions per message |
| Input validation | Validate all message types |
| Rate limiting | Per-connection limits |
| Message size | Maximum 64KB |
| Timeout | Heartbeat with 90s timeout |
Secure WebSocket Server
1// src/websocket/server.ts2import { WebSocketServer } from 'ws';3import { IncomingMessage } from 'http';4import { verifyTerminalToken } from '../auth/token';56const ALLOWED_ORIGINS = [7 'https://app.example.com',8 'https://terminal.example.com',9];1011const MAX_MESSAGE_SIZE = 64 * 1024; // 64KB12const HEARTBEAT_INTERVAL = 30000;13const HEARTBEAT_TIMEOUT = 90000;1415export function createSecureWebSocketServer(server: any) {16 const wss = new WebSocketServer({17 server,18 verifyClient: async (info, callback) => {19 try {20 // 1. Validate origin21 const origin = info.req.headers.origin;22 if (!origin || !ALLOWED_ORIGINS.includes(origin)) {23 callback(false, 403, 'Invalid origin');24 return;25 }2627 // 2. Extract and verify token28 const url = new URL(info.req.url!, `https://${info.req.headers.host}`);29 const token = url.searchParams.get('token');3031 if (!token) {32 callback(false, 401, 'Token required');33 return;34 }3536 const payload = await verifyTerminalToken(token);3738 // Attach user info to request for later use39 (info.req as any).user = payload;4041 callback(true);42 } catch (error) {43 console.error('WebSocket auth failed:', error);44 callback(false, 403, 'Invalid token');45 }46 },47 maxPayload: MAX_MESSAGE_SIZE,48 });4950 wss.on('connection', (ws, req: IncomingMessage) => {51 const user = (req as any).user;52 console.log(`User ${user.sub} connected`);5354 // Set up heartbeat55 let isAlive = true;56 ws.on('pong', () => { isAlive = true; });5758 const heartbeat = setInterval(() => {59 if (!isAlive) {60 console.log(`User ${user.sub} heartbeat timeout`);61 ws.terminate();62 return;63 }64 isAlive = false;65 ws.ping();66 }, HEARTBEAT_INTERVAL);6768 ws.on('close', () => {69 clearInterval(heartbeat);70 });7172 // Rate limiting per connection73 const rateLimiter = createConnectionRateLimiter(100, 60000); // 100 msg/min7475 ws.on('message', async (data) => {76 // Check rate limit77 if (!rateLimiter.check()) {78 ws.send(JSON.stringify({79 type: 'error',80 code: 'RATE_LIMITED',81 message: 'Too many messages',82 }));83 return;84 }8586 await handleMessage(ws, user, data);87 });88 });8990 return wss;91}9293function createConnectionRateLimiter(limit: number, window: number) {94 let count = 0;95 let resetTime = Date.now() + window;9697 return {98 check(): boolean {99 const now = Date.now();100 if (now > resetTime) {101 count = 0;102 resetTime = now + window;103 }104 count++;105 return count <= limit;106 },107 };108}Input Validation
1// Validate all incoming messages2interface InputMessage {3 type: 'input' | 'resize' | 'ping';4 data?: string;5 cols?: number;6 rows?: number;7}89function validateMessage(raw: Buffer): InputMessage | null {10 try {11 const msg = JSON.parse(raw.toString());1213 // Validate type14 if (!['input', 'resize', 'ping'].includes(msg.type)) {15 return null;16 }1718 // Validate input data19 if (msg.type === 'input') {20 if (typeof msg.data !== 'string' || msg.data.length > 1024) {21 return null;22 }23 }2425 // Validate resize dimensions26 if (msg.type === 'resize') {27 if (28 !Number.isInteger(msg.cols) ||29 !Number.isInteger(msg.rows) ||30 msg.cols < 1 || msg.cols > 500 ||31 msg.rows < 1 || msg.rows > 20032 ) {33 return null;34 }35 }3637 return msg;38 } catch {39 return null;40 }41}Part 3: Session Management
Session State Machine
Session Manager
1// src/session/manager.ts2import { randomUUID } from 'crypto';34interface Session {5 id: string;6 userId: string;7 workspaceId: string;8 token: string;9 state: SessionState;10 createdAt: Date;11 lastActivity: Date;12 expiresAt: Date;13}1415type SessionState =16 | 'authenticated'17 | 'active'18 | 'idle'19 | 'refreshing'20 | 'expired'21 | 'terminated';2223class SessionManager {24 private sessions = new Map<string, Session>();25 private userSessions = new Map<string, Set<string>>();2627 async create(28 userId: string,29 workspaceId: string,30 token: string,31 expiresAt: Date32 ): Promise<Session> {33 const session: Session = {34 id: randomUUID(),35 userId,36 workspaceId,37 token,38 state: 'authenticated',39 createdAt: new Date(),40 lastActivity: new Date(),41 expiresAt,42 };4344 this.sessions.set(session.id, session);4546 // Track user sessions for concurrent limit47 const userSessionSet = this.userSessions.get(userId) || new Set();48 userSessionSet.add(session.id);49 this.userSessions.set(userId, userSessionSet);5051 return session;52 }5354 async activate(sessionId: string): Promise<void> {55 const session = this.sessions.get(sessionId);56 if (!session) throw new Error('Session not found');5758 // Regenerate session ID on activation (security best practice)59 const newId = randomUUID();60 session.id = newId;61 session.state = 'active';62 session.lastActivity = new Date();6364 this.sessions.delete(sessionId);65 this.sessions.set(newId, session);6667 // Update user sessions map68 const userSessions = this.userSessions.get(session.userId);69 if (userSessions) {70 userSessions.delete(sessionId);71 userSessions.add(newId);72 }73 }7475 async updateActivity(sessionId: string): Promise<void> {76 const session = this.sessions.get(sessionId);77 if (session && session.state === 'active') {78 session.lastActivity = new Date();79 }80 }8182 async refreshToken(83 sessionId: string,84 newToken: string,85 newExpiresAt: Date86 ): Promise<void> {87 const session = this.sessions.get(sessionId);88 if (!session) throw new Error('Session not found');8990 session.token = newToken;91 session.expiresAt = newExpiresAt;92 session.state = 'active';93 }9495 async terminate(sessionId: string): Promise<void> {96 const session = this.sessions.get(sessionId);97 if (!session) return;9899 session.state = 'terminated';100 this.sessions.delete(sessionId);101102 const userSessions = this.userSessions.get(session.userId);103 if (userSessions) {104 userSessions.delete(sessionId);105 }106 }107108 async terminateAllUserSessions(userId: string): Promise<void> {109 const sessionIds = this.userSessions.get(userId);110 if (!sessionIds) return;111112 for (const sessionId of sessionIds) {113 await this.terminate(sessionId);114 }115116 this.userSessions.delete(userId);117 }118119 // Get active session count for user (for limiting concurrent sessions)120 getActiveSessionCount(userId: string): number {121 const sessions = this.userSessions.get(userId);122 return sessions?.size || 0;123 }124}125126export const sessionManager = new SessionManager();Part 4: Container Isolation
For terminal workloads, container isolation is critical.
Isolation Technologies Comparison
| Technology | Startup | Overhead | Isolation Level | Best For |
|---|---|---|---|---|
| Standard Docker | Fast | Low | Shared kernel | Trusted code |
| gVisor | 50-100ms | 10-20% | User-space kernel | Multi-tenant |
| Kata Containers | 150-300ms | Higher | Full VM | Untrusted code |
| Firecracker | 100-200ms | Minimal | MicroVM | Serverless |
gVisor Configuration
1# Install gVisor runtime2curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg3echo "deb [arch=amd64 signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list4sudo apt-get update && sudo apt-get install -y runsc56# Configure Docker to use gVisor7cat << EOF | sudo tee /etc/docker/daemon.json8{9 "runtimes": {10 "runsc": {11 "path": "/usr/bin/runsc"12 }13 }14}15EOF1617sudo systemctl restart dockerRunning Terminal Containers with gVisor
1// Use gVisor runtime for untrusted terminal sessions2const container = await docker.createContainer({3 Image: 'workstation:latest',4 HostConfig: {5 Runtime: 'runsc', // Use gVisor6 Memory: 2 * 1024 * 1024 * 1024,7 NanoCpus: 1e9,8 ReadonlyRootfs: true,9 CapDrop: ['ALL'],10 SecurityOpt: ['no-new-privileges'],11 },12});Security Layers Diagram
Part 5: Content Security Policy
CSP for Terminal Pages
1// CSP headers for terminal application2const cspDirectives = {3 'default-src': ["'self'"],4 'script-src': ["'self'", "'unsafe-eval'"], // xterm.js needs eval5 'style-src': ["'self'", "'unsafe-inline'"], // xterm.js inline styles6 'connect-src': ["'self'", 'wss://terminal.example.com'],7 'frame-ancestors': ["'self'"],8 'frame-src': ["'none'"],9 'object-src': ["'none'"],10 'base-uri': ["'self'"],11 'form-action': ["'self'"],12};1314function generateCSP(directives: Record<string, string[]>): string {15 return Object.entries(directives)16 .map(([key, values]) => `${key} ${values.join(' ')}`)17 .join('; ');18}1920// Apply to responses21response.headers.set('Content-Security-Policy', generateCSP(cspDirectives));Additional Security Headers
1const securityHeaders = {2 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',3 'X-Content-Type-Options': 'nosniff',4 'X-Frame-Options': 'SAMEORIGIN',5 'X-XSS-Protection': '1; mode=block',6 'Referrer-Policy': 'strict-origin-when-cross-origin',7 'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',8};Part 6: Audit Logging
What to Log
| Event | Data to Capture |
|---|---|
| Session created | User ID, workspace, IP, timestamp |
| Commands executed | Command hash (not full content), timestamp |
| Session terminated | Reason, duration, bytes transferred |
| Authentication failed | Attempted user, IP, failure reason |
| Rate limit hit | User ID, endpoint, limit exceeded |
Audit Logger
1// src/audit/logger.ts2interface AuditEvent {3 timestamp: Date;4 eventType: string;5 userId?: string;6 sessionId?: string;7 ip?: string;8 userAgent?: string;9 details: Record<string, unknown>;10}1112class AuditLogger {13 async log(event: Omit<AuditEvent, 'timestamp'>): Promise<void> {14 const auditEvent: AuditEvent = {15 ...event,16 timestamp: new Date(),17 };1819 // Log to secure storage (never log to console in production)20 await this.persist(auditEvent);21 }2223 async logSessionCreated(24 userId: string,25 sessionId: string,26 ip: string27 ): Promise<void> {28 await this.log({29 eventType: 'SESSION_CREATED',30 userId,31 sessionId,32 ip,33 details: {},34 });35 }3637 async logCommandExecuted(38 sessionId: string,39 commandHash: string40 ): Promise<void> {41 await this.log({42 eventType: 'COMMAND_EXECUTED',43 sessionId,44 details: { commandHash }, // Never log actual commands45 });46 }4748 async logAuthFailure(49 ip: string,50 reason: string,51 attemptedUser?: string52 ): Promise<void> {53 await this.log({54 eventType: 'AUTH_FAILURE',55 ip,56 userId: attemptedUser,57 details: { reason },58 });59 }6061 private async persist(event: AuditEvent): Promise<void> {62 // Send to secure logging service63 // Examples: AWS CloudWatch, Datadog, Splunk64 }65}6667export const auditLogger = new AuditLogger();Part 7: Security Checklist
Pre-Production Checklist
Authentication:
- JWT tokens with short expiry (15-60 minutes)
- Token refresh mechanism implemented
- Token revocation capability
- Secure token storage (HttpOnly cookies or memory)
WebSocket Security:
- WSS only (TLS 1.3)
- Origin validation
- Token validation on connect
- Rate limiting per connection
- Message size limits
- Heartbeat monitoring
Session Management:
- Session ID regeneration on privilege change
- Concurrent session limits
- Automatic session timeout
- Secure session termination
Container Isolation:
- Non-root user
- Dropped capabilities
- Read-only root filesystem
- Resource limits (CPU, memory, PIDs)
- Network isolation
- Consider gVisor/Kata for untrusted code
Infrastructure:
- WAF rules configured
- DDoS protection enabled
- Security headers set
- Audit logging enabled
- Secrets in secure storage
Brisbane Security Consulting
At Buun Group, we secure cloud development infrastructure for Queensland businesses:
- Security audits for browser-based terminals
- Authentication architecture with JWT and OAuth
- Container hardening with gVisor and Kata
- Compliance consulting for Australian regulations
We've secured production terminal systems serving thousands of users.
Need secure terminal infrastructure?
Topics
Comments
Sign in to join the conversation
LoginNo comments yet. Be the first to share your thoughts!
Found an issue with this article?
