Skip to main content
/ Cloud

Securing Browser Terminals: JWT Auth, WebSocket Security, and Sessions

Sacha Roussakis-NotterSacha Roussakis-Notter
15 min read
Docker
TypeScript
Share

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.

pie chart
Terminal Security Breach Causes (2025)
Container Escape
Other
Session Hijacking
Unauthenticated WebSocket
Weak Token Security
Hover for detailsbuun.group

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.

flowchart

Security Layers

Attack Surface

Unauthenticated Access

Session Hijacking

Command Injection

WebSocket Abuse

Container Escape

JWT Authentication

Session Management

Input Validation

WebSocket Security

Container Isolation

Ctrl+scroll to zoom • Drag to pan40%

Part 1: JWT Authentication for Terminal Sessions

Token Structure

typescript
1// JWT payload for terminal sessions
2interface TerminalTokenPayload {
3 sub: string; // User ID
4 email: string; // User email
5 workspaceId: string; // Target workspace
6 permissions: string[]; // e.g., ['terminal:read', 'terminal:write']
7 iat: number; // Issued at
8 exp: number; // Expiration
9 jti: string; // Unique token ID (for revocation)
10}

Token Generation

typescript
1// src/auth/token.ts
2import { SignJWT, jwtVerify } from 'jose';
3import { randomUUID } from 'crypto';
4
5const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
6const ISSUER = 'terminal-service';
7const AUDIENCE = 'terminal-client';
8
9export async function generateTerminalToken(
10 userId: string,
11 email: string,
12 workspaceId: string
13): Promise<{ token: string; expiresAt: Date }> {
14 const expiresAt = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes
15
16 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);
29
30 return { token, expiresAt };
31}
32
33export async function verifyTerminalToken(
34 token: string
35): Promise<TerminalTokenPayload> {
36 const { payload } = await jwtVerify(token, SECRET, {
37 issuer: ISSUER,
38 audience: AUDIENCE,
39 });
40
41 return payload as TerminalTokenPayload;
42}

Token Refresh Flow

sequence
Terminal ServerAuth ServerClientToken expires in 30 minutesPOST /auth/terminal-tokenValidate session{ token, expiresAt }WSS connect with tokenVerify JWTConnection establishedToken expiring soonPOST /auth/refresh-token{ newToken, expiresAt }Send new tokenUpdate session token
Ctrl+scroll to zoom • Drag to pan34%

Part 2: WebSocket Security

OWASP WebSocket Security Checklist

RequirementImplementation
EncryptionWSS only (TLS 1.3)
Origin validationCheck Origin header
AuthenticationValidate JWT on connect
AuthorizationCheck permissions per message
Input validationValidate all message types
Rate limitingPer-connection limits
Message sizeMaximum 64KB
TimeoutHeartbeat with 90s timeout

Secure WebSocket Server

typescript
1// src/websocket/server.ts
2import { WebSocketServer } from 'ws';
3import { IncomingMessage } from 'http';
4import { verifyTerminalToken } from '../auth/token';
5
6const ALLOWED_ORIGINS = [
7 'https://app.example.com',
8 'https://terminal.example.com',
9];
10
11const MAX_MESSAGE_SIZE = 64 * 1024; // 64KB
12const HEARTBEAT_INTERVAL = 30000;
13const HEARTBEAT_TIMEOUT = 90000;
14
15export function createSecureWebSocketServer(server: any) {
16 const wss = new WebSocketServer({
17 server,
18 verifyClient: async (info, callback) => {
19 try {
20 // 1. Validate origin
21 const origin = info.req.headers.origin;
22 if (!origin || !ALLOWED_ORIGINS.includes(origin)) {
23 callback(false, 403, 'Invalid origin');
24 return;
25 }
26
27 // 2. Extract and verify token
28 const url = new URL(info.req.url!, `https://${info.req.headers.host}`);
29 const token = url.searchParams.get('token');
30
31 if (!token) {
32 callback(false, 401, 'Token required');
33 return;
34 }
35
36 const payload = await verifyTerminalToken(token);
37
38 // Attach user info to request for later use
39 (info.req as any).user = payload;
40
41 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 });
49
50 wss.on('connection', (ws, req: IncomingMessage) => {
51 const user = (req as any).user;
52 console.log(`User ${user.sub} connected`);
53
54 // Set up heartbeat
55 let isAlive = true;
56 ws.on('pong', () => { isAlive = true; });
57
58 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);
67
68 ws.on('close', () => {
69 clearInterval(heartbeat);
70 });
71
72 // Rate limiting per connection
73 const rateLimiter = createConnectionRateLimiter(100, 60000); // 100 msg/min
74
75 ws.on('message', async (data) => {
76 // Check rate limit
77 if (!rateLimiter.check()) {
78 ws.send(JSON.stringify({
79 type: 'error',
80 code: 'RATE_LIMITED',
81 message: 'Too many messages',
82 }));
83 return;
84 }
85
86 await handleMessage(ws, user, data);
87 });
88 });
89
90 return wss;
91}
92
93function createConnectionRateLimiter(limit: number, window: number) {
94 let count = 0;
95 let resetTime = Date.now() + window;
96
97 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

typescript
1// Validate all incoming messages
2interface InputMessage {
3 type: 'input' | 'resize' | 'ping';
4 data?: string;
5 cols?: number;
6 rows?: number;
7}
8
9function validateMessage(raw: Buffer): InputMessage | null {
10 try {
11 const msg = JSON.parse(raw.toString());
12
13 // Validate type
14 if (!['input', 'resize', 'ping'].includes(msg.type)) {
15 return null;
16 }
17
18 // Validate input data
19 if (msg.type === 'input') {
20 if (typeof msg.data !== 'string' || msg.data.length > 1024) {
21 return null;
22 }
23 }
24
25 // Validate resize dimensions
26 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 > 200
32 ) {
33 return null;
34 }
35 }
36
37 return msg;
38 } catch {
39 return null;
40 }
41}

Part 3: Session Management

Session State Machine

state diagram

Valid JWT

Invalid JWT

WebSocket connected

Inactivity timeout

User activity

Extended inactivity

Token expiring

New token received

Refresh failed

User logout

Session cleared

ANONYMOUS

AUTHENTICATED

ACTIVE

IDLE

EXPIRED

REFRESHING

TERMINATED

Regenerate session ID

30 second window

Ctrl+scroll to zoom • Drag to pan39%

Session Manager

typescript
1// src/session/manager.ts
2import { randomUUID } from 'crypto';
3
4interface 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}
14
15type SessionState =
16 | 'authenticated'
17 | 'active'
18 | 'idle'
19 | 'refreshing'
20 | 'expired'
21 | 'terminated';
22
23class SessionManager {
24 private sessions = new Map<string, Session>();
25 private userSessions = new Map<string, Set<string>>();
26
27 async create(
28 userId: string,
29 workspaceId: string,
30 token: string,
31 expiresAt: Date
32 ): 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 };
43
44 this.sessions.set(session.id, session);
45
46 // Track user sessions for concurrent limit
47 const userSessionSet = this.userSessions.get(userId) || new Set();
48 userSessionSet.add(session.id);
49 this.userSessions.set(userId, userSessionSet);
50
51 return session;
52 }
53
54 async activate(sessionId: string): Promise<void> {
55 const session = this.sessions.get(sessionId);
56 if (!session) throw new Error('Session not found');
57
58 // 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();
63
64 this.sessions.delete(sessionId);
65 this.sessions.set(newId, session);
66
67 // Update user sessions map
68 const userSessions = this.userSessions.get(session.userId);
69 if (userSessions) {
70 userSessions.delete(sessionId);
71 userSessions.add(newId);
72 }
73 }
74
75 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 }
81
82 async refreshToken(
83 sessionId: string,
84 newToken: string,
85 newExpiresAt: Date
86 ): Promise<void> {
87 const session = this.sessions.get(sessionId);
88 if (!session) throw new Error('Session not found');
89
90 session.token = newToken;
91 session.expiresAt = newExpiresAt;
92 session.state = 'active';
93 }
94
95 async terminate(sessionId: string): Promise<void> {
96 const session = this.sessions.get(sessionId);
97 if (!session) return;
98
99 session.state = 'terminated';
100 this.sessions.delete(sessionId);
101
102 const userSessions = this.userSessions.get(session.userId);
103 if (userSessions) {
104 userSessions.delete(sessionId);
105 }
106 }
107
108 async terminateAllUserSessions(userId: string): Promise<void> {
109 const sessionIds = this.userSessions.get(userId);
110 if (!sessionIds) return;
111
112 for (const sessionId of sessionIds) {
113 await this.terminate(sessionId);
114 }
115
116 this.userSessions.delete(userId);
117 }
118
119 // 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}
125
126export const sessionManager = new SessionManager();

Part 4: Container Isolation

For terminal workloads, container isolation is critical.

Isolation Technologies Comparison

bar chart
Container Isolation Startup Time (ms)
overhead
startup
DockergVisorFirecrackerKata0ms60ms120ms180ms240ms
Hover for detailsbuun.group
TechnologyStartupOverheadIsolation LevelBest For
Standard DockerFastLowShared kernelTrusted code
gVisor50-100ms10-20%User-space kernelMulti-tenant
Kata Containers150-300msHigherFull VMUntrusted code
Firecracker100-200msMinimalMicroVMServerless

gVisor Configuration

bash
1# Install gVisor runtime
2curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
3echo "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.list
4sudo apt-get update && sudo apt-get install -y runsc
5
6# Configure Docker to use gVisor
7cat << EOF | sudo tee /etc/docker/daemon.json
8{
9 "runtimes": {
10 "runsc": {
11 "path": "/usr/bin/runsc"
12 }
13 }
14}
15EOF
16
17sudo systemctl restart docker

Running Terminal Containers with gVisor

typescript
1// Use gVisor runtime for untrusted terminal sessions
2const container = await docker.createContainer({
3 Image: 'workstation:latest',
4 HostConfig: {
5 Runtime: 'runsc', // Use gVisor
6 Memory: 2 * 1024 * 1024 * 1024,
7 NanoCpus: 1e9,
8 ReadonlyRootfs: true,
9 CapDrop: ['ALL'],
10 SecurityOpt: ['no-new-privileges'],
11 },
12});

Security Layers Diagram

flowchart

Host Protection

SELinux/AppArmor

Seccomp Profiles

Container Isolation

gVisor Runtime

Dropped Capabilities

Network Isolation

Application Layer

JWT Validation

Session Manager

Edge Security

WAF Rules

Rate Limiting

Browser Sandbox

WebSocket Client

Ctrl+scroll to zoom • Drag to pan21%

Part 5: Content Security Policy

CSP for Terminal Pages

typescript
1// CSP headers for terminal application
2const cspDirectives = {
3 'default-src': ["'self'"],
4 'script-src': ["'self'", "'unsafe-eval'"], // xterm.js needs eval
5 'style-src': ["'self'", "'unsafe-inline'"], // xterm.js inline styles
6 '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};
13
14function generateCSP(directives: Record<string, string[]>): string {
15 return Object.entries(directives)
16 .map(([key, values]) => `${key} ${values.join(' ')}`)
17 .join('; ');
18}
19
20// Apply to responses
21response.headers.set('Content-Security-Policy', generateCSP(cspDirectives));

Additional Security Headers

typescript
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

EventData to Capture
Session createdUser ID, workspace, IP, timestamp
Commands executedCommand hash (not full content), timestamp
Session terminatedReason, duration, bytes transferred
Authentication failedAttempted user, IP, failure reason
Rate limit hitUser ID, endpoint, limit exceeded

Audit Logger

typescript
1// src/audit/logger.ts
2interface 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}
11
12class AuditLogger {
13 async log(event: Omit<AuditEvent, 'timestamp'>): Promise<void> {
14 const auditEvent: AuditEvent = {
15 ...event,
16 timestamp: new Date(),
17 };
18
19 // Log to secure storage (never log to console in production)
20 await this.persist(auditEvent);
21 }
22
23 async logSessionCreated(
24 userId: string,
25 sessionId: string,
26 ip: string
27 ): Promise<void> {
28 await this.log({
29 eventType: 'SESSION_CREATED',
30 userId,
31 sessionId,
32 ip,
33 details: {},
34 });
35 }
36
37 async logCommandExecuted(
38 sessionId: string,
39 commandHash: string
40 ): Promise<void> {
41 await this.log({
42 eventType: 'COMMAND_EXECUTED',
43 sessionId,
44 details: { commandHash }, // Never log actual commands
45 });
46 }
47
48 async logAuthFailure(
49 ip: string,
50 reason: string,
51 attemptedUser?: string
52 ): Promise<void> {
53 await this.log({
54 eventType: 'AUTH_FAILURE',
55 ip,
56 userId: attemptedUser,
57 details: { reason },
58 });
59 }
60
61 private async persist(event: AuditEvent): Promise<void> {
62 // Send to secure logging service
63 // Examples: AWS CloudWatch, Datadog, Splunk
64 }
65}
66
67export 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

browser terminal securityWebSocket securityJWT authenticationterminal session managementcontainer isolationgVisor securityOWASP WebSocketcloud IDE security

Share this post

Share

Comments

Sign in to join the conversation

Login

No comments yet. Be the first to share your thoughts!

Found an issue with this article?

/ Let's Talk

Want to work with us?

Whether you need help with architecture, development, or technical consulting, our team is here to help bring your vision to life.