Skip to content

Implement WebSocket Authentication for Enhanced Security#33

Closed
DeveloperAmrit wants to merge 3 commits intoAOSSIE-Org:mainfrom
DeveloperAmrit:fix-25-authrce
Closed

Implement WebSocket Authentication for Enhanced Security#33
DeveloperAmrit wants to merge 3 commits intoAOSSIE-Org:mainfrom
DeveloperAmrit:fix-25-authrce

Conversation

@DeveloperAmrit
Copy link
Contributor

@DeveloperAmrit DeveloperAmrit commented Feb 8, 2026

Fixes #25

Summary
This PR addresses a critical security vulnerability in the WebSocket server by implementing an authentication mechanism to prevent unauthorized access and control. The changes ensure that only authenticated clients can send commands to the server, mitigating the risk of malicious actors exploiting the open WebSocket connection.

Changes Implemented

  1. Server-Side Enhancements
  • PIN-Based Authentication:
    • A random 6-digit PIN is generated when the server starts.
    • Localhost connections are automatically authenticated, while remote connections require the PIN.
  • Authentication Handling:
    • Clients must send the correct PIN via an authenticate message to gain access.
    • Unauthenticated clients are blocked from sending commands.
  • Connection Feedback:
    • The server sends a connected message to clients, indicating whether authentication is required and providing the PIN for local clients.
  1. Client-Side Updates
  • Connection Hook (useRemoteConnection):
    • Added state management for authentication (isAuthenticated, authRequired, authError).
    • Exposed an authenticate(pin) function to handle PIN submission.
    • Enhanced WebSocket message handling to process authentication responses.
  1. UI Updates
  • Settings Page:
    • Displays the session PIN prominently for the user to share with remote devices.
  • Trackpad Page:
    • Added a PIN entry screen for remote clients when authentication is required.
    • Displays error messages for incorrect PIN submissions.
  1. Security Improvements:
  • Blocked unauthenticated clients from executing commands (e.g., mouse/keyboard control).
  • Ensured that only authenticated clients can interact with the server.

Testing

  • Verified that the server generates a unique PIN for each session.
  • Tested localhost connections to ensure automatic authentication.
  • Tested remote connections to ensure authentication is required and enforced.
  • Verified that unauthenticated clients are blocked from sending commands.
  • Confirmed that the UI updates dynamically based on authentication state.

Screenshot (server side)
image

Video (client side)

WhatsApp.Video.2026-02-08.at.07.53.53.mp4

Summary by CodeRabbit

  • New Features

    • PIN-based authentication for remote device access with auto-generated session PIN.
    • Session PIN banner in settings so users can view the current PIN.
    • PIN entry UI with 6-digit validation and Connect action.
    • Automatic authentication for local connections.
    • Enhanced connection states: connecting (loading) and disconnected views.
  • Bug Fixes / UX

    • Immediate feedback for invalid PIN attempts (error alerts).

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

Adds a PIN-based WebSocket authentication flow: server generates a 6‑digit session PIN, auto-authenticates local clients, and requires remote clients to authenticate. Client hook and UI updated to handle auth handshake, state, and error handling; server enforces authentication before processing control messages.

Changes

Cohort / File(s) Summary
Server: websocket & auth
src/server/websocket.ts
Generates a 6‑digit session PIN, tracks per-connection auth state (WeakMap), auto-authenticates local clients, marks remote clients as authRequired, handles authenticate messages, responds with auth-success / auth-failed, and enforces auth for subsequent messages.
Client hook: WebSocket state + auth
src/hooks/useRemoteConnection.ts
Adds isAuthenticated, authRequired, authError, message parsing for connected/auth-success/auth-failed, error handling, authenticate(pin) method, and clears auth state on close.
Client UI: PIN flows + display
src/routes/trackpad.tsx, src/routes/settings.tsx
Trackpad: adds PIN input, validation, auth UI branch, invokes authenticate(), and shows error alerts. Settings: captures and displays session PIN when provided by server.

Sequence Diagram(s)

sequenceDiagram
    participant Mobile as Mobile Client
    participant Server as WebSocket Server
    participant LocalApp as Local Client (Desktop)

    Mobile->>Server: CONNECT ws://host:port/ws
    Server->>Server: generate 6-digit PIN\nidentify remote vs local
    alt remote client
        Server->>Mobile: { type: "connected", authRequired: true }
        Mobile->>Mobile: show PIN entry UI
        Mobile->>Server: { type: "authenticate", pin: "XXXXXX" }
        alt PIN matches
            Server->>Mobile: { type: "auth-success" }
            Mobile->>Mobile: set isAuthenticated = true
        else PIN mismatch
            Server->>Mobile: { type: "auth-failed" }
            Mobile->>Mobile: show auth error
        end
    else local client
        Server->>LocalApp: { type: "connected", pin: "XXXXXX", authRequired: false }
        LocalApp->>LocalApp: display PIN to user (settings)
    end
    Mobile->>Server: { type: "text" / "mouse" / ... }
    Server->>Server: verify authenticated -> process message
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Six digits tucked in a hop and a grin,
I guard the keys so no sneaky one wins.
Local friends stay snug, remotes type the code,
Together we keep the connection in mode. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: implementing WebSocket authentication for security, which directly addresses the RCE vulnerability outlined in the linked issue.
Linked Issues check ✅ Passed All coding objectives from issue #25 are met: PIN generation, automatic localhost authentication, remote client PIN requirement, authentication handshake with authenticate message, blocking unauthenticated clients from executing commands, and displaying PIN to local users.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing authentication as specified in issue #25; no unrelated modifications to unrelated features or dependencies.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useRemoteConnection.ts (1)

65-68: ⚠️ Potential issue | 🟠 Major

Stale ws reference in cleanup closure.

The cleanup function captures ws from the initial render (always null) because the dependency array is []. The clearTimeout works correctly since reconnectTimer is scoped inside the effect, but ws?.close() will never actually close the socket on unmount.

Proposed fix — use a ref or close via the local `socket` variable

One approach is to track the socket in a ref:

+    const wsRef = useRef<WebSocket | null>(null);
+
     useEffect(() => {
         ...
         const connect = () => {
             ...
             const socket = new WebSocket(wsUrl);
+            wsRef.current = socket;
             ...
-            setWs(socket);
+            setWs(socket);
         };

         connect();

         return () => {
             clearTimeout(reconnectTimer);
-            ws?.close();
+            wsRef.current?.close();
         };
     }, []);
🤖 Fix all issues with AI agents
In `@src/hooks/useRemoteConnection.ts`:
- Around line 50-55: The socket.onclose handler currently clears status,
isAuthenticated and authRequired but leaves authError true; update the onclose
block in useRemoteConnection (socket.onclose) to also reset authError by calling
setAuthError(false) so the PIN screen doesn’t show a stale "Incorrect PIN" after
reconnect; optionally also ensure socket.onopen/handleConnect clears authError
as well to be safe during successful reconnects.

In `@src/routes/settings.tsx`:
- Around line 98-109: The SVG used inside the PIN alert (the JSX block that
renders when the variable `pin` is truthy) is missing an accessible title; add a
unique <title> element and reference it from the SVG via aria-labelledby (or
aria-label) so screen readers can announce the icon—e.g., insert a descriptive
title like "Info" (with an id such as "session-pin-icon-title") and set
aria-labelledby="session-pin-icon-title" (and keep role="img" if needed) on the
same <svg> element that currently renders the info icon in the Session PIN
alert.

In `@src/routes/trackpad.tsx`:
- Line 37: The two inline SVG elements in src/routes/trackpad.tsx (the
disconnected icon around line 37 and the error icon around line 78) are missing
accessible labels; update each SVG element to include an appropriate aria-label
(e.g., aria-label="Disconnected" and aria-label="Error") and role="img" so
assistive tech can read them (do not leave aria-hidden). Locate the <svg ...>
elements for those icons in the JSX and add these attributes to the SVG tags.
- Around line 68-74: The Connect button currently relies on the default button
type which is "submit" and can cause unintended form submissions; update the
button element that calls authenticate(pinInput) (the onClick handler
referencing authenticate and pinInput) to include an explicit type="button"
attribute so it won't submit any enclosing form implicitly.
- Around line 55-65: The PIN input currently accepts non-digits and allows Enter
to trigger authenticate without length validation; update the input handlers so
onChange for the input (bound to pinInput via setPinInput) filters out any
non-digit characters and truncates to 6 characters before calling setPinInput,
and update the onKeyDown handler to only call authenticate(pinInput) when
pinInput.length >= 6 (and optionally preventDefault for Enter when length < 6)
to match the button disabled logic; reference the input element using the
existing value={pinInput}, onChange={e => setPinInput(...)} and onKeyDown={(e)
=> { if (e.key === 'Enter') authenticate(pinInput); }} handlers and the
authenticate function.

In `@src/server/websocket.ts`:
- Around line 76-87: Add per-connection rate limiting for PIN authentication:
track failed attempts for each WebSocket (e.g., a Map like authFailCount keyed
by ws) when handling msg.type === 'authenticate'; increment the count on each
wrong PIN (compare against PIN), send the auth-failed message, and if count
exceeds a MAX_FAILED_ATTEMPTS constant (e.g., 5) then send a final failure
message and close the socket (ws.close()) and/or set a short timeout before
allowing further attempts; optionally add a small delay (e.g., setTimeout) after
each failed attempt to slow brute-force. Ensure successful auth resets/clears
the failure count and still sets clientAuth.set(ws, true).
🧹 Nitpick comments (2)
src/server/websocket.ts (2)

26-29: Math.random() is not cryptographically secure.

For a session PIN, Math.random() is predictable if the attacker can observe or infer the PRNG state. Consider using crypto.randomInt from Node's crypto module for better security, especially since this is a security-critical feature.

Proposed fix
+import { randomInt } from 'crypto';
+
-const PIN = Math.floor(100000 + Math.random() * 900000).toString();
+const PIN = randomInt(100000, 1000000).toString();

89-93: Silently dropping messages from unauthenticated clients — consider sending an error response.

The commented-out error response (line 91) would help clients understand why their commands are being ignored. Without it, debugging connection issues for remote clients will be difficult.

Proposed fix
 if (!clientAuth.get(ws)) {
-    // ws.send(JSON.stringify({ type: 'error', message: 'Authentication required' }));
+    ws.send(JSON.stringify({ type: 'error', message: 'Authentication required' }));
     return;
 }

Comment on lines 50 to 55
socket.onclose = () => {
setStatus('disconnected');
setIsAuthenticated(false);
setAuthRequired(false);
reconnectTimer = setTimeout(connect, 3000);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

authError is not reset on reconnection.

When the WebSocket closes and reconnects, isAuthenticated and authRequired are cleared but authError remains true. After reconnection, the PIN entry screen will immediately show the "Incorrect PIN" error from the previous session.

Proposed fix
 socket.onclose = () => {
     setStatus('disconnected');
     setIsAuthenticated(false);
     setAuthRequired(false);
+    setAuthError(false);
     reconnectTimer = setTimeout(connect, 3000);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
socket.onclose = () => {
setStatus('disconnected');
setIsAuthenticated(false);
setAuthRequired(false);
reconnectTimer = setTimeout(connect, 3000);
};
socket.onclose = () => {
setStatus('disconnected');
setIsAuthenticated(false);
setAuthRequired(false);
setAuthError(false);
reconnectTimer = setTimeout(connect, 3000);
};
🤖 Prompt for AI Agents
In `@src/hooks/useRemoteConnection.ts` around lines 50 - 55, The socket.onclose
handler currently clears status, isAuthenticated and authRequired but leaves
authError true; update the onclose block in useRemoteConnection (socket.onclose)
to also reset authError by calling setAuthError(false) so the PIN screen doesn’t
show a stale "Incorrect PIN" after reconnect; optionally also ensure
socket.onopen/handleConnect clears authError as well to be safe during
successful reconnects.

Comment on lines +98 to +109
{pin && (
<div className="alert alert-info shadow-lg">
<div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current flex-shrink-0 w-6 h-6"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div>
<h3 className="font-bold">Session PIN</h3>
<div className="text-xs">Enter this PIN on your mobile device</div>
</div>
</div>
<div className="font-mono text-4xl font-black">{pin}</div>
</div>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add accessible title to the SVG icon.

The SVG on line 101 lacks an accessible title element, flagged by Biome's noSvgWithoutTitle rule.

Proposed fix
-<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current flex-shrink-0 w-6 h-6"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current flex-shrink-0 w-6 h-6" aria-label="Info"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{pin && (
<div className="alert alert-info shadow-lg">
<div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current flex-shrink-0 w-6 h-6"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div>
<h3 className="font-bold">Session PIN</h3>
<div className="text-xs">Enter this PIN on your mobile device</div>
</div>
</div>
<div className="font-mono text-4xl font-black">{pin}</div>
</div>
)}
{pin && (
<div className="alert alert-info shadow-lg">
<div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current flex-shrink-0 w-6 h-6" aria-label="Info"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div>
<h3 className="font-bold">Session PIN</h3>
<div className="text-xs">Enter this PIN on your mobile device</div>
</div>
</div>
<div className="font-mono text-4xl font-black">{pin}</div>
</div>
)}
🧰 Tools
🪛 Biome (2.3.13)

[error] 101-101: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

🤖 Prompt for AI Agents
In `@src/routes/settings.tsx` around lines 98 - 109, The SVG used inside the PIN
alert (the JSX block that renders when the variable `pin` is truthy) is missing
an accessible title; add a unique <title> element and reference it from the SVG
via aria-labelledby (or aria-label) so screen readers can announce the
icon—e.g., insert a descriptive title like "Info" (with an id such as
"session-pin-icon-title") and set aria-labelledby="session-pin-icon-title" (and
keep role="img" if needed) on the same <svg> element that currently renders the
info icon in the Session PIN alert.

<div className="h-full flex items-center justify-center p-6 bg-base-100">
<div className="text-center space-y-4">
<div className="text-error">
<svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add accessible labels to SVG icons.

Both SVGs (disconnected icon on line 37 and error icon on line 78) lack accessible text, flagged by Biome's noSvgWithoutTitle. Add aria-label attributes.

Also applies to: 78-78

🧰 Tools
🪛 Biome (2.3.13)

[error] 37-37: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

🤖 Prompt for AI Agents
In `@src/routes/trackpad.tsx` at line 37, The two inline SVG elements in
src/routes/trackpad.tsx (the disconnected icon around line 37 and the error icon
around line 78) are missing accessible labels; update each SVG element to
include an appropriate aria-label (e.g., aria-label="Disconnected" and
aria-label="Error") and role="img" so assistive tech can read them (do not leave
aria-hidden). Locate the <svg ...> elements for those icons in the JSX and add
these attributes to the SVG tags.

Comment on lines +55 to +65
<input
type="tel"
className="input input-bordered w-full text-center text-4xl tracking-[0.5em] font-mono h-16"
placeholder="000000"
maxLength={6}
value={pinInput}
onChange={e => setPinInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') authenticate(pinInput);
}}
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

PIN input allows non-digit characters, and Enter bypasses the 6-digit length check.

Two issues with the PIN input:

  1. type="tel" doesn't restrict to digits only on all browsers — non-numeric characters can be entered.
  2. Pressing Enter (line 63) calls authenticate(pinInput) without checking pinInput.length >= 6, unlike the button which is disabled until 6 characters.
Proposed fix
 <input
     type="tel"
     className="input input-bordered w-full text-center text-4xl tracking-[0.5em] font-mono h-16"
     placeholder="000000"
     maxLength={6}
     value={pinInput}
-    onChange={e => setPinInput(e.target.value)}
+    onChange={e => setPinInput(e.target.value.replace(/\D/g, ''))}
     onKeyDown={(e) => {
-        if (e.key === 'Enter') authenticate(pinInput);
+        if (e.key === 'Enter' && pinInput.length === 6) authenticate(pinInput);
     }}
 />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input
type="tel"
className="input input-bordered w-full text-center text-4xl tracking-[0.5em] font-mono h-16"
placeholder="000000"
maxLength={6}
value={pinInput}
onChange={e => setPinInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') authenticate(pinInput);
}}
/>
<input
type="tel"
className="input input-bordered w-full text-center text-4xl tracking-[0.5em] font-mono h-16"
placeholder="000000"
maxLength={6}
value={pinInput}
onChange={e => setPinInput(e.target.value.replace(/\D/g, ''))}
onKeyDown={(e) => {
if (e.key === 'Enter' && pinInput.length === 6) authenticate(pinInput);
}}
/>
🤖 Prompt for AI Agents
In `@src/routes/trackpad.tsx` around lines 55 - 65, The PIN input currently
accepts non-digits and allows Enter to trigger authenticate without length
validation; update the input handlers so onChange for the input (bound to
pinInput via setPinInput) filters out any non-digit characters and truncates to
6 characters before calling setPinInput, and update the onKeyDown handler to
only call authenticate(pinInput) when pinInput.length >= 6 (and optionally
preventDefault for Enter when length < 6) to match the button disabled logic;
reference the input element using the existing value={pinInput}, onChange={e =>
setPinInput(...)} and onKeyDown={(e) => { if (e.key === 'Enter')
authenticate(pinInput); }} handlers and the authenticate function.

Comment on lines +68 to +74
<button
className="btn btn-primary w-full max-w-xs btn-lg"
onClick={() => authenticate(pinInput)}
disabled={pinInput.length < 6}
>
Connect
</button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit type="button" to prevent implicit form submission.

Biome flags this button as missing an explicit type prop. Without it, the default is "submit", which can cause unintended form submission if the DOM structure changes.

Proposed fix
 <button
     className="btn btn-primary w-full max-w-xs btn-lg"
     onClick={() => authenticate(pinInput)}
     disabled={pinInput.length < 6}
+    type="button"
 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
className="btn btn-primary w-full max-w-xs btn-lg"
onClick={() => authenticate(pinInput)}
disabled={pinInput.length < 6}
>
Connect
</button>
<button
className="btn btn-primary w-full max-w-xs btn-lg"
onClick={() => authenticate(pinInput)}
disabled={pinInput.length < 6}
type="button"
>
Connect
</button>
🧰 Tools
🪛 Biome (2.3.13)

[error] 68-72: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
In `@src/routes/trackpad.tsx` around lines 68 - 74, The Connect button currently
relies on the default button type which is "submit" and can cause unintended
form submissions; update the button element that calls authenticate(pinInput)
(the onClick handler referencing authenticate and pinInput) to include an
explicit type="button" attribute so it won't submit any enclosing form
implicitly.

Comment on lines +76 to +87
// Handle Authentication
if (msg.type === 'authenticate') {
if (msg.pin === PIN) {
clientAuth.set(ws, true);
ws.send(JSON.stringify({ type: 'auth-success' }));
console.log('Client authenticated successfully.');
} else {
ws.send(JSON.stringify({ type: 'auth-failed' }));
console.log('Client failed authentication.');
}
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

No rate limiting on PIN authentication attempts — brute-force risk.

A 6-digit PIN has only 10⁶ possible values. Without rate limiting or lockout, a remote attacker can rapidly enumerate all PINs over the WebSocket. At thousands of attempts per second, the PIN could be cracked in under a minute.

Consider adding per-connection attempt limits (e.g., disconnect after 5 failures) or a short delay after each failed attempt.

Proposed fix — disconnect after max failed attempts
+const MAX_AUTH_ATTEMPTS = 5;
+const authAttempts = new WeakMap<WebSocket, number>();

 // In the connection handler:
+authAttempts.set(ws, 0);

 // In the authenticate handler:
 if (msg.type === 'authenticate') {
+    const attempts = (authAttempts.get(ws) || 0) + 1;
+    authAttempts.set(ws, attempts);
     if (msg.pin === PIN) {
         clientAuth.set(ws, true);
         ws.send(JSON.stringify({ type: 'auth-success' }));
         console.log('Client authenticated successfully.');
     } else {
         ws.send(JSON.stringify({ type: 'auth-failed' }));
-        console.log('Client failed authentication.');
+        console.log(`Client failed authentication. Attempt ${attempts}/${MAX_AUTH_ATTEMPTS}`);
+        if (attempts >= MAX_AUTH_ATTEMPTS) {
+            console.log('Max auth attempts reached. Disconnecting client.');
+            ws.close();
+        }
     }
     return;
 }
🤖 Prompt for AI Agents
In `@src/server/websocket.ts` around lines 76 - 87, Add per-connection rate
limiting for PIN authentication: track failed attempts for each WebSocket (e.g.,
a Map like authFailCount keyed by ws) when handling msg.type === 'authenticate';
increment the count on each wrong PIN (compare against PIN), send the
auth-failed message, and if count exceeds a MAX_FAILED_ATTEMPTS constant (e.g.,
5) then send a final failure message and close the socket (ws.close()) and/or
set a short timeout before allowing further attempts; optionally add a small
delay (e.g., setTimeout) after each failed attempt to slow brute-force. Ensure
successful auth resets/clears the failure count and still sets
clientAuth.set(ws, true).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/settings.tsx (1)

197-223: ⚠️ Potential issue | 🟠 Major

Reuse the authenticated WebSocket connection to send update-config.

The settings page creates a new, unauthenticated WebSocket and immediately sends update-config. The server enforces authentication (lines 89–92 of src/server/websocket.ts) and silently drops unauthenticated messages. For remote clients, this means the config update fails silently with no error feedback.

The useRemoteConnection hook already provides an authenticated connection with proper handshake handling—reuse it instead of creating a new socket. This ensures the command is processed and handles both local and remote scenarios correctly.

🧹 Nitpick comments (2)
src/routes/settings.tsx (1)

63-73: Socket not closed after receiving connected message.

The server-ip handler (line 78) closes the socket after receiving the IP, but the connected handler does not. This leaves the detection socket open until component unmount. Since this socket is only used for initial detection, close it after extracting the data.

Proposed fix
                 if (data.type === 'connected') {
                     if (data.serverIp) {
                         console.log('Auto-detected IP:', data.serverIp);
                         setIp(data.serverIp);
                     }
                     if (data.pin) {
                         console.log('Received session PIN');
                         setPin(data.pin);
                     }
+                    socket.close();
                 }
src/routes/trackpad.tsx (1)

47-85: authError alert persists while user retypes — consider clearing on input change.

When the user enters an incorrect PIN, the error alert stays visible as they type a new PIN. Clearing authError on input change would improve UX. This would require the hook to expose a clearAuthError or reset it internally when authenticate is called.

@imxade
Copy link
Contributor

imxade commented Feb 16, 2026

create a new pr as per feedback in the issue itself

@imxade imxade closed this Feb 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Unauthenticated Remote Input Execution (RCE risk)

2 participants

Comments