Merge pull request #587 from HKUDS/fix/whatsapp-bridge-security
fix(security): bind WhatsApp bridge to localhost + optional token auth
This commit is contained in:
commit
92191ad2a9
@ -95,8 +95,8 @@ File operations have path traversal protection, but:
|
|||||||
- Consider using a firewall to restrict outbound connections if needed
|
- Consider using a firewall to restrict outbound connections if needed
|
||||||
|
|
||||||
**WhatsApp Bridge:**
|
**WhatsApp Bridge:**
|
||||||
- The bridge runs on `localhost:3001` by default
|
- The bridge binds to `127.0.0.1:3001` (localhost only, not accessible from external network)
|
||||||
- If exposing to network, use proper authentication and TLS
|
- Set `bridgeToken` in config to enable shared-secret authentication between Python and Node.js
|
||||||
- Keep authentication data in `~/.nanobot/whatsapp-auth` secure (mode 0700)
|
- Keep authentication data in `~/.nanobot/whatsapp-auth` secure (mode 0700)
|
||||||
|
|
||||||
### 6. Dependency Security
|
### 6. Dependency Security
|
||||||
@ -224,7 +224,7 @@ If you suspect a security breach:
|
|||||||
✅ **Secure Communication**
|
✅ **Secure Communication**
|
||||||
- HTTPS for all external API calls
|
- HTTPS for all external API calls
|
||||||
- TLS for Telegram API
|
- TLS for Telegram API
|
||||||
- WebSocket security for WhatsApp bridge
|
- WhatsApp bridge: localhost-only binding + optional token auth
|
||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
|
|||||||
@ -25,11 +25,12 @@ import { join } from 'path';
|
|||||||
|
|
||||||
const PORT = parseInt(process.env.BRIDGE_PORT || '3001', 10);
|
const PORT = parseInt(process.env.BRIDGE_PORT || '3001', 10);
|
||||||
const AUTH_DIR = process.env.AUTH_DIR || join(homedir(), '.nanobot', 'whatsapp-auth');
|
const AUTH_DIR = process.env.AUTH_DIR || join(homedir(), '.nanobot', 'whatsapp-auth');
|
||||||
|
const TOKEN = process.env.BRIDGE_TOKEN || undefined;
|
||||||
|
|
||||||
console.log('🐈 nanobot WhatsApp Bridge');
|
console.log('🐈 nanobot WhatsApp Bridge');
|
||||||
console.log('========================\n');
|
console.log('========================\n');
|
||||||
|
|
||||||
const server = new BridgeServer(PORT, AUTH_DIR);
|
const server = new BridgeServer(PORT, AUTH_DIR, TOKEN);
|
||||||
|
|
||||||
// Handle graceful shutdown
|
// Handle graceful shutdown
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* WebSocket server for Python-Node.js bridge communication.
|
* WebSocket server for Python-Node.js bridge communication.
|
||||||
|
* Security: binds to 127.0.0.1 only; optional BRIDGE_TOKEN auth.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { WebSocketServer, WebSocket } from 'ws';
|
import { WebSocketServer, WebSocket } from 'ws';
|
||||||
@ -21,12 +22,13 @@ export class BridgeServer {
|
|||||||
private wa: WhatsAppClient | null = null;
|
private wa: WhatsAppClient | null = null;
|
||||||
private clients: Set<WebSocket> = new Set();
|
private clients: Set<WebSocket> = new Set();
|
||||||
|
|
||||||
constructor(private port: number, private authDir: string) {}
|
constructor(private port: number, private authDir: string, private token?: string) {}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
// Create WebSocket server
|
// Bind to localhost only — never expose to external network
|
||||||
this.wss = new WebSocketServer({ port: this.port });
|
this.wss = new WebSocketServer({ host: '127.0.0.1', port: this.port });
|
||||||
console.log(`🌉 Bridge server listening on ws://localhost:${this.port}`);
|
console.log(`🌉 Bridge server listening on ws://127.0.0.1:${this.port}`);
|
||||||
|
if (this.token) console.log('🔒 Token authentication enabled');
|
||||||
|
|
||||||
// Initialize WhatsApp client
|
// Initialize WhatsApp client
|
||||||
this.wa = new WhatsAppClient({
|
this.wa = new WhatsAppClient({
|
||||||
@ -38,7 +40,34 @@ export class BridgeServer {
|
|||||||
|
|
||||||
// Handle WebSocket connections
|
// Handle WebSocket connections
|
||||||
this.wss.on('connection', (ws) => {
|
this.wss.on('connection', (ws) => {
|
||||||
|
if (this.token) {
|
||||||
|
// Require auth handshake as first message
|
||||||
|
const timeout = setTimeout(() => ws.close(4001, 'Auth timeout'), 5000);
|
||||||
|
ws.once('message', (data) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(data.toString());
|
||||||
|
if (msg.type === 'auth' && msg.token === this.token) {
|
||||||
|
console.log('🔗 Python client authenticated');
|
||||||
|
this.setupClient(ws);
|
||||||
|
} else {
|
||||||
|
ws.close(4003, 'Invalid token');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ws.close(4003, 'Invalid auth message');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
console.log('🔗 Python client connected');
|
console.log('🔗 Python client connected');
|
||||||
|
this.setupClient(ws);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to WhatsApp
|
||||||
|
await this.wa.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupClient(ws: WebSocket): void {
|
||||||
this.clients.add(ws);
|
this.clients.add(ws);
|
||||||
|
|
||||||
ws.on('message', async (data) => {
|
ws.on('message', async (data) => {
|
||||||
@ -61,10 +90,6 @@ export class BridgeServer {
|
|||||||
console.error('WebSocket error:', error);
|
console.error('WebSocket error:', error);
|
||||||
this.clients.delete(ws);
|
this.clients.delete(ws);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Connect to WhatsApp
|
|
||||||
await this.wa.connect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleCommand(cmd: SendCommand): Promise<void> {
|
private async handleCommand(cmd: SendCommand): Promise<void> {
|
||||||
|
|||||||
@ -42,6 +42,9 @@ class WhatsAppChannel(BaseChannel):
|
|||||||
try:
|
try:
|
||||||
async with websockets.connect(bridge_url) as ws:
|
async with websockets.connect(bridge_url) as ws:
|
||||||
self._ws = ws
|
self._ws = ws
|
||||||
|
# Send auth token if configured
|
||||||
|
if self.config.bridge_token:
|
||||||
|
await ws.send(json.dumps({"type": "auth", "token": self.config.bridge_token}))
|
||||||
self._connected = True
|
self._connected = True
|
||||||
logger.info("Connected to WhatsApp bridge")
|
logger.info("Connected to WhatsApp bridge")
|
||||||
|
|
||||||
|
|||||||
@ -636,14 +636,20 @@ def _get_bridge_dir() -> Path:
|
|||||||
def channels_login():
|
def channels_login():
|
||||||
"""Link device via QR code."""
|
"""Link device via QR code."""
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from nanobot.config.loader import load_config
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
bridge_dir = _get_bridge_dir()
|
bridge_dir = _get_bridge_dir()
|
||||||
|
|
||||||
console.print(f"{__logo__} Starting bridge...")
|
console.print(f"{__logo__} Starting bridge...")
|
||||||
console.print("Scan the QR code to connect.\n")
|
console.print("Scan the QR code to connect.\n")
|
||||||
|
|
||||||
|
env = {**os.environ}
|
||||||
|
if config.channels.whatsapp.bridge_token:
|
||||||
|
env["BRIDGE_TOKEN"] = config.channels.whatsapp.bridge_token
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.run(["npm", "start"], cwd=bridge_dir, check=True)
|
subprocess.run(["npm", "start"], cwd=bridge_dir, check=True, env=env)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
console.print(f"[red]Bridge failed: {e}[/red]")
|
console.print(f"[red]Bridge failed: {e}[/red]")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
|||||||
@ -9,6 +9,7 @@ class WhatsAppConfig(BaseModel):
|
|||||||
"""WhatsApp channel configuration."""
|
"""WhatsApp channel configuration."""
|
||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
bridge_url: str = "ws://localhost:3001"
|
bridge_url: str = "ws://localhost:3001"
|
||||||
|
bridge_token: str = "" # Shared token for bridge auth (optional, recommended)
|
||||||
allow_from: list[str] = Field(default_factory=list) # Allowed phone numbers
|
allow_from: list[str] = Field(default_factory=list) # Allowed phone numbers
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user