
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
sshclient-wasm
Advanced tools
WebAssembly-based SSH client for the browser with packet interception hooks
WebAssembly-based SSH client for the browser with transport-agnostic architecture. Built with Go's golang.org/x/crypto/ssh package and compiled to WASM for browser usage.
graph TB
subgraph "Browser Environment"
A["ES Module<br/>(TypeScript)"]
B["Transport Translation Layer"]
C["WASM Layer<br/>(Go SSH Client)"]
A --> B
B <--> C
end
subgraph "Transport Implementations"
D["WebSocket Transport"]
E["AWS IoT Secure Tunnel<br/>Transport"]
F["Custom Transport<br/>(User-defined)"]
B --> D
B --> E
B --> F
end
subgraph "Network Layer"
G["Direct WebSocket<br/>Connection"]
H["AWS IoT<br/>Secure Tunneling"]
I["Custom Protocol<br/>Endpoint"]
D <--> G
E <--> H
F <--> I
end
subgraph "Destination"
J["SSH Server"]
G --> J
H --> J
I --> J
end
style A fill:#0277bd,color:#fff
style B fill:#7b1fa2,color:#fff
style C fill:#ef6c00,color:#fff
style D fill:#388e3c,color:#fff
style E fill:#388e3c,color:#fff
style F fill:#388e3c,color:#fff
style G fill:#d32f2f,color:#fff
style H fill:#d32f2f,color:#fff
style I fill:#d32f2f,color:#fff
style J fill:#424242,color:#fff
npm install sshclient-wasm
npm install sshclient-wasm
Copy WASM files to your public directory:
# Copy these files to your public/ directory:
# - sshclient.wasm
# - wasm_exec.js
// Next.js optimized
import { initializeSSHClient, SSHClient, NextJSConfig } from "sshclient-wasm/next";
// Vite optimized
import { initializeSSHClient, SSHClient, ViteConfig } from "sshclient-wasm/vite";
// React hooks and utilities
import { useSSHClient, SSHClient } from "sshclient-wasm/react";
// Generic/universal import
import { SSHClient } from "sshclient-wasm";
// Framework-specific (auto-optimized)
import { initializeSSHClient } from "sshclient-wasm/next";
await initializeSSHClient();
// Or generic with auto-detection
import { SSHClient } from "sshclient-wasm";
await SSHClient.initialize();
import { SSHClient, WebSocketTransport } from "sshclient-wasm";
// Simple initialization - auto-detects assets in public directory
await SSHClient.initialize();
// Create a WebSocket transport
const transport = new WebSocketTransport(
"transport-1",
"wss://ssh-gateway.example.com",
["ssh"]
);
// Connect to SSH server
const session = await SSHClient.connect(
{
host: "example.com",
port: 22,
user: "username",
password: "password",
},
transport
);
// Send data
await session.send(new Uint8Array([0x01, 0x02, 0x03]));
// Disconnect
await session.disconnect();
import { initializeSSHClient, NextJSConfig } from "sshclient-wasm/next";
import { useEffect, useState } from "react";
function SSHComponent() {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
// Initialize with Next.js optimizations
initializeSSHClient()
.then(() => setIsReady(true))
.catch((error) => {
console.error("Failed to initialize SSH client:", error);
// Error messages will guide you to fix asset placement
});
}, []);
if (!isReady) return <div>Loading SSH client...</div>;
// Your SSH logic here
return <div>SSH client ready!</div>;
}
// Alternative: Use the React hook
import { useSSHClient } from "sshclient-wasm/react";
function SSHComponentWithHook() {
const { isInitialized, initError, isLoading } = useSSHClient();
if (isLoading) return <div>Loading SSH client...</div>;
if (initError) return <div>Error: {initError.message}</div>;
if (!isInitialized) return <div>SSH client not ready</div>;
return <div>SSH client ready!</div>;
}
import { initializeSSHClient, ViteConfig } from "sshclient-wasm/vite";
import { useSSHConnection } from "sshclient-wasm/react";
function ViteSSHComponent() {
const { connect, disconnect, session, connectionState } = useSSHConnection();
useEffect(() => {
initializeSSHClient();
}, []);
const handleConnect = async () => {
const transport = new WebSocketTransport("transport-1", "wss://example.com");
await connect({
host: "example.com",
port: 22,
user: "username",
password: "password"
}, transport);
};
return (
<div>
<p>Status: {connectionState}</p>
<button onClick={handleConnect}>Connect SSH</button>
<button onClick={disconnect}>Disconnect</button>
</div>
);
}
import { SSHClient, SecureTunnelTransport } from "sshclient-wasm";
// Auto-initialize
await SSHClient.initialize();
// Create AWS IoT Secure Tunnel transport
const transport = new SecureTunnelTransport("tunnel-1", {
region: "us-east-1",
accessToken: "your-tunnel-access-token",
clientMode: "source",
serviceId: "SSH",
protocol: "V3",
});
// Connect through the secure tunnel
const session = await SSHClient.connect(
{
host: "internal-server",
port: 22,
user: "username",
privateKey: "ssh-private-key",
},
transport
);
// Use the SSH session
await session.send(encoder.encode("ls -la\n"));
// Disconnect
await session.disconnect();
const transport = new WebSocketTransport("transport-1", "wss://example.com");
const session = await SSHClient.connect(
{
host: "example.com",
port: 22,
user: "username",
password: "password",
},
transport,
{
onPacketSend: (data, metadata) => {
console.log("Sending packet:", data, metadata);
},
onPacketReceive: (data, metadata) => {
console.log("Received packet:", data, metadata);
},
onStateChange: (state) => {
console.log("Connection state:", state);
},
}
);
import { PacketTransformer } from "sshclient-wasm";
// Transform to/from Base64
const base64 = PacketTransformer.toBase64(data);
const binary = PacketTransformer.fromBase64(base64);
// Custom Protobuf transformation (implement your own logic)
const protobuf = PacketTransformer.toProtobuf(data, schema);
1. Copy WASM files to public/:
cp node_modules/sshclient-wasm/dist/sshclient.wasm public/
cp node_modules/sshclient-wasm/dist/wasm_exec.js public/
2. Use the Next.js configuration helper:
// next.config.js
import { NextJSConfig } from "sshclient-wasm/next";
/** @type {import('next').NextConfig} */
const nextConfig = NextJSConfig.getNextConfig({
// Your custom Next.js config here
});
module.exports = nextConfig;
Or configure manually:
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
],
},
];
},
};
module.exports = nextConfig;
3. Use in your components:
import { initializeSSHClient, useSSHClient } from "sshclient-wasm/next";
// Method 1: Direct initialization
useEffect(() => {
initializeSSHClient()
.then(() => console.log("SSH client ready"))
.catch(console.error);
}, []);
// Method 2: Using the hook (if using sshclient-wasm/react)
const { isInitialized, initError, isLoading } = useSSHClient();
1. Copy WASM files to public/:
cp node_modules/sshclient-wasm/dist/sshclient.wasm public/
cp node_modules/sshclient-wasm/dist/wasm_exec.js public/
2. Use the Vite configuration helper:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { ViteConfig } from "sshclient-wasm/vite";
export default defineConfig(ViteConfig.getViteConfig({
plugins: [react()],
// Your custom Vite config here
}));
Or configure manually:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
headers: {
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
},
},
});
3. Use in your components:
import { initializeSSHClient } from "sshclient-wasm/vite";
import { useSSHClient } from "sshclient-wasm/react";
// Method 1: Direct initialization
useEffect(() => {
initializeSSHClient()
.then(() => console.log("SSH client ready"))
.catch(console.error);
}, []);
// Method 2: Using React hooks
const { isInitialized, initError, isLoading } = useSSHClient();
The sshclient-wasm/react module provides React-specific hooks and utilities:
import {
useSSHClient,
useSSHConnection,
SSHClientProvider,
withSSHClient
} from "sshclient-wasm/react";
// Hook for initialization
const { isInitialized, initError, isLoading } = useSSHClient();
// Hook for connection management
const { connect, disconnect, send, session, connectionState } = useSSHConnection();
// Provider for context
<SSHClientProvider options={{ cacheBusting: false }}>
<YourApp />
</SSHClientProvider>
// HOC wrapper
const WrappedComponent = withSSHClient(YourComponent);
1. Copy WASM files to your public directory
2. Initialize with custom options:
import { SSHClient } from "sshclient-wasm";
await SSHClient.initialize({
publicDir: "/assets/", // Your public directory path
autoDetect: true,
cacheBusting: process.env.NODE_ENV === "development",
});
import { SSHClient } from "sshclient-wasm";
await SSHClient.initialize({
wasmPath: "/custom/path/sshclient.wasm",
wasmExecPath: "/custom/path/wasm_exec.js",
autoDetect: false,
cacheBusting: false,
timeout: 15000,
});
import { SSHClientHelpers } from "sshclient-wasm";
// Test if assets are properly placed
const { wasmAvailable, wasmExecAvailable } =
await SSHClientHelpers.testAssetAvailability(
"/sshclient.wasm",
"/wasm_exec.js"
);
if (!wasmAvailable) {
console.error("❌ Please copy sshclient.wasm to your public directory");
}
if (!wasmExecAvailable) {
console.error("❌ Please copy wasm_exec.js to your public directory");
}
try {
await SSHClient.initialize();
} catch (error) {
if (error.message.includes("WASM file not found")) {
console.error("Copy sshclient.wasm to public/ directory");
} else if (error.message.includes("wasm_exec.js not found")) {
console.error("Copy wasm_exec.js to public/ directory");
} else if (error.message.includes("Failed to fetch WASM")) {
console.error("Check network connection and CORS headers");
} else {
console.error("Initialization failed:", error.message);
}
}
# Clone the repository
git clone https://github.com/andrew/sshclient-wasm.git
cd sshclient-wasm
# Install dependencies
go mod download
npm install
# Build WASM and TypeScript
make build
# Run example
make dev
sshclient-wasm/
├── main.go # WASM entry point
├── pkg/sshclient/ # Go SSH client implementation
│ ├── client.go # Main client logic
│ └── interceptor.go # Packet interception
├── lib/ # TypeScript/JavaScript bindings
│ └── index.ts # Main TypeScript API
├── example/ # Example application
└── dist/ # Build output
The main entry point for establishing SSH connections through WebAssembly.
class SSHClient {
/**
* Initialize the WASM module. Must be called before any other methods.
* @param options - Initialization options or legacy string path
*/
static async initialize(
options?: InitializationOptions | string
): Promise<void>;
/**
* Connect to an SSH server through a transport
* @param options - SSH connection configuration
* @param transport - Transport implementation for network communication
* @param callbacks - Optional callbacks for monitoring packet flow
* @returns SSH session handle for sending data and disconnecting
*/
static async connect(
options: ConnectionOptions,
transport: Transport,
callbacks?: SSHClientCallbacks
): Promise<SSHSession>;
/**
* Disconnect a specific SSH session
* @param sessionId - The ID of the session to disconnect
*/
static async disconnect(sessionId: string): Promise<void>;
/**
* Get the library version
* @returns Version string
*/
static getVersion(): string;
}
Utilities for transforming SSH packet data.
class PacketTransformer {
/**
* Convert binary data to Base64 encoding
* @param data - Binary data to encode
* @returns Base64 encoded string
*/
static toBase64(data: Uint8Array): string;
/**
* Convert Base64 string to binary data
* @param base64 - Base64 encoded string
* @returns Decoded binary data
*/
static fromBase64(base64: string): Uint8Array;
/**
* Transform binary data to Protobuf format (user-implemented)
* @param data - Binary data to transform
* @param schema - Protobuf schema definition
* @returns Protobuf encoded data
*/
static toProtobuf(data: Uint8Array, schema?: any): Uint8Array;
/**
* Transform Protobuf data to binary format (user-implemented)
* @param data - Protobuf encoded data
* @param schema - Protobuf schema definition
* @returns Decoded binary data
*/
static fromProtobuf(data: Uint8Array, schema?: any): Uint8Array;
}
Direct WebSocket connection to SSH servers.
class WebSocketTransport implements Transport {
/**
* Create a WebSocket transport
* @param id - Unique identifier for this transport
* @param url - WebSocket URL (e.g., 'wss://ssh-gateway.example.com')
* @param protocols - Optional WebSocket subprotocols
*/
constructor(id: string, url: string, protocols?: string[]);
async connect(): Promise<void>;
async disconnect(): Promise<void>;
async send(data: Uint8Array): Promise<void>;
}
AWS IoT Secure Tunneling transport for end-to-end encrypted connections.
class SecureTunnelTransport implements Transport {
/**
* Create an AWS IoT Secure Tunnel transport
* @param id - Unique identifier for this transport
* @param config - Tunnel configuration
*/
constructor(id: string, config: SecureTunnelConfig);
async connect(): Promise<void>;
async disconnect(): Promise<void>;
async send(data: Uint8Array): Promise<void>;
}
User-defined transport for custom protocols.
class CustomTransport implements Transport {
/**
* Create a custom transport with user-provided implementations
* @param id - Unique identifier for this transport
* @param connectImpl - Custom connection implementation
* @param disconnectImpl - Custom disconnection implementation
* @param sendImpl - Custom send implementation
*/
constructor(
id: string,
connectImpl?: () => Promise<void>,
disconnectImpl?: () => Promise<void>,
sendImpl?: (data: Uint8Array) => Promise<void>
);
/**
* Inject received data into the transport
* @param data - Data received from the custom protocol
*/
injectData(data: Uint8Array): void;
}
Framework-specific helpers and utilities.
class SSHClientHelpers {
/**
* Get recommended asset paths for the detected framework
*/
static getAssetPaths: (publicDir?: string) => {
wasmPath: string;
wasmExecPath: string;
};
/**
* Detect the current framework
*/
static detectFramework: () => "nextjs" | "vite" | "webpack" | "generic";
/**
* Test if WASM assets are available at the given paths
*/
static testAssetAvailability: (
wasmPath: string,
wasmExecPath: string
) => Promise<{
wasmAvailable: boolean;
wasmExecAvailable: boolean;
}>;
/**
* Next.js specific initialization helper
*/
static initializeForNextJS: (
options?: Partial<InitializationOptions>
) => Promise<void>;
/**
* Vite specific initialization helper
*/
static initializeForVite: (
options?: Partial<InitializationOptions>
) => Promise<void>;
/**
* Generic initialization with sensible defaults
*/
static initializeWithDefaults: (
customOptions?: Partial<InitializationOptions>
) => Promise<void>;
}
Configuration options for initializing the SSH client.
interface InitializationOptions {
/** Path to the WASM file (default: auto-detected) */
wasmPath?: string;
/** Path to the wasm_exec.js file (default: auto-detected) */
wasmExecPath?: string;
/** Enable automatic path detection (default: true) */
autoDetect?: boolean;
/** Public directory path for auto-detection (default: '/') */
publicDir?: string;
/** Enable cache busting for development (default: true) */
cacheBusting?: boolean;
/** Timeout for loading assets in milliseconds (default: 10000) */
timeout?: number;
}
Base interface for all transport implementations.
interface Transport {
id: string;
connect(): Promise<void>;
disconnect(): Promise<void>;
send(data: Uint8Array): Promise<void>;
// Event handlers (set by the library)
onData?: (data: Uint8Array) => void;
onError?: (error: Error) => void;
onClose?: () => void;
}
SSH connection configuration.
interface ConnectionOptions {
/** Target SSH server hostname */
host: string;
/** SSH server port (default: 22) */
port: number;
/** SSH username */
user: string;
/** Password for authentication (optional) */
password?: string;
/** Private key for authentication (optional) */
privateKey?: string;
/** Connection timeout in milliseconds (optional) */
timeout?: number;
}
AWS IoT Secure Tunnel configuration.
interface SecureTunnelConfig {
/** AWS region where the tunnel is created */
region: string;
/** Tunnel access token */
accessToken: string;
/** Client mode: 'source' or 'destination' */
clientMode: "source" | "destination";
/** Service identifier for multiplexed tunnels (optional) */
serviceId?: string;
/** Protocol version: 'V2' or 'V3' (default: 'V2') */
protocol?: "V2" | "V3";
}
SSH session handle returned from connect().
interface SSHSession {
/** Unique session identifier */
sessionId: string;
/**
* Send data through the SSH connection
* @param data - Binary data to send
*/
send(data: Uint8Array): Promise<void>;
/**
* Close the SSH connection
*/
disconnect(): Promise<void>;
}
Optional callbacks for monitoring SSH packet flow.
interface SSHClientCallbacks {
/**
* Called when sending an SSH packet
* @param data - Raw packet data being sent
* @param metadata - Packet metadata (type, size, etc.)
*/
onPacketSend?: (data: Uint8Array, metadata: PacketMetadata) => void;
/**
* Called when receiving an SSH packet
* @param data - Raw packet data received
* @param metadata - Packet metadata (type, size, etc.)
*/
onPacketReceive?: (data: Uint8Array, metadata: PacketMetadata) => void;
/**
* Called when SSH connection state changes
* @param state - New connection state
*/
onStateChange?: (state: SSHConnectionState) => void;
}
Metadata about SSH packets.
interface PacketMetadata {
/** Packet type identifier */
packetType: string;
/** Packet type code */
packetTypeCode: number;
/** Packet size in bytes */
size: number;
/** Timestamp of packet */
timestamp?: number;
}
SSH connection states.
type SSHConnectionState =
| "connecting"
| "connected"
| "authenticating"
| "authenticated"
| "ready"
| "disconnecting"
| "disconnected"
| "error";
AWS IoT Secure Tunnel message types.
enum TunnelMessageType {
UNKNOWN = 0,
DATA = 1,
STREAM_START = 2,
STREAM_RESET = 3,
SESSION_RESET = 4,
SERVICE_IDS = 5,
CONNECTION_START = 6,
CONNECTION_RESET = 7,
}
# Solution: Copy WASM files to public directory
cp node_modules/sshclient-wasm/dist/sshclient.wasm public/
cp node_modules/sshclient-wasm/dist/wasm_exec.js public/
wasm_exec.js is accessible and loads successfullyNext.js users: Add CORS headers to next.config.js:
async headers() {
return [{
source: '/(.*)',
headers: [
{ key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
],
}];
}
Vite users: Add to vite.config.ts:
server: {
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
}
import { SSHClientHelpers } from "sshclient-wasm";
const result = await SSHClientHelpers.testAssetAvailability(
"/sshclient.wasm",
"/wasm_exec.js"
);
console.log("WASM available:", result.wasmAvailable);
console.log("Exec available:", result.wasmExecAvailable);
import { SSHClientHelpers } from "sshclient-wasm";
console.log("Detected framework:", SSHClientHelpers.detectFramework());
console.log("Recommended paths:", SSHClientHelpers.getAssetPaths());
All async methods throw errors that can be caught:
try {
await SSHClient.initialize();
await SSHClient.connect(options, transport);
} catch (error) {
if (error.message.includes("WASM file not found")) {
console.error("📁 Copy sshclient.wasm to public/ directory");
} else if (error.message.includes("wasm_exec.js not found")) {
console.error("📁 Copy wasm_exec.js to public/ directory");
} else if (error.message.includes("Failed to fetch WASM")) {
console.error("🌐 Check network connection and CORS headers");
} else if (error.message.includes("authentication")) {
console.error("🔐 Check SSH credentials");
} else if (error.message.includes("timeout")) {
console.error("⏰ Connection or initialization timeout");
} else {
console.error("❌ Unknown error:", error.message);
}
}
const session = await SSHClient.connect(connectionOptions, transport, {
onPacketSend: (data, metadata) => {
// Transform outgoing packets
const transformed = PacketTransformer.toProtobuf(data, mySchema);
console.log(
`Sending ${metadata.packetType} packet (${metadata.size} bytes)`
);
return transformed;
},
onPacketReceive: (data, metadata) => {
// Process incoming packets
const decoded = PacketTransformer.fromProtobuf(data, mySchema);
console.log(`Received ${metadata.packetType} packet`);
return decoded;
},
});
class MyCustomTransport extends CustomTransport {
constructor(id: string, config: MyConfig) {
super(
id,
async () => {
// Custom connection logic
await this.establishConnection(config);
},
async () => {
// Custom disconnection logic
await this.closeConnection();
},
async (data: Uint8Array) => {
// Custom send logic
await this.sendData(data);
}
);
}
private async establishConnection(config: MyConfig) {
// Implementation specific to your protocol
}
// Call this.injectData() when receiving data from your protocol
handleIncomingData(data: Uint8Array) {
this.injectData(data);
}
}
BSD-3-Clause
FAQs
WebAssembly-based SSH client for the browser with packet interception hooks
We found that sshclient-wasm demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.