Skip to main content

Overview

Security is critical when working with FTP, especially in production environments. This guide covers TLS encryption, credential management, and Cloudflare Workers-specific security practices.

Use TLS/FTPS

Always use TLS encryption for FTP connections to protect data in transit:
const client = new FTPClient('ftp.example.com', {
  user: 'username',
  pass: 'password',
  secure: true  // Enable TLS
});

await client.connect();

What TLS protects

1

Control channel encryption

All FTP commands and responses are encrypted after the AUTH TLS handshake.
2

Data channel encryption

File transfers are encrypted when PROT P (protection level private) is enabled.
3

Credential protection

Username and password are transmitted securely over the encrypted connection.
4

Man-in-the-middle prevention

TLS certificate validation prevents attackers from intercepting traffic.

TLS implementation details

The FTPClient implements TLS using Cloudflare’s startTls() method from src/classes/ftp-client.ts:136:
if (this.opts.secure) {
  if (!this.feats.AUTH || !this.feats.AUTH.includes("TLS")) {
    console.warn(
      "Server does not advertise STARTTLS yet it was requested.\nAttempting anyways...",
    );
  }
  status = await this.command(Commands.Auth, "TLS");
  this.assertStatus(StatusCodes.AuthProceed, status, this.conn);
  
  this.reader?.releaseLock();
  this.reader = undefined;
  this.conn = this.conn.startTls({
    expectedServerHostname: this.host,
  });
  
  // Enable data channel protection
  status = await this.command(Commands.Protection, "P");
  this.assertStatus(StatusCodes.OK, status, this.conn);
}
If the server doesn’t support TLS (AUTH TLS or PROT P not in FEAT response), the connection attempt will log a warning but continue. Some servers may reject the connection. Always verify your server supports TLS before enabling it.

Credential management

Never hardcode credentials in your code. Use environment variables or Cloudflare Workers secrets.

Cloudflare Workers secrets

Store FTP credentials as secrets in Cloudflare Workers:
# Add secrets using wrangler CLI
wrangler secret put FTP_USER
wrangler secret put FTP_PASS
wrangler secret put FTP_HOST

TypeScript types for environment

Define types for your environment variables:
interface Env {
  FTP_HOST: string;
  FTP_USER: string;
  FTP_PASS: string;
  FTP_PORT?: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const client = new FTPClient(env.FTP_HOST, {
      user: env.FTP_USER,
      pass: env.FTP_PASS,
      port: env.FTP_PORT ? parseInt(env.FTP_PORT) : 21,
      secure: true
    });
    
    // ...
  }
};
Cloudflare Workers secrets are encrypted at rest and only decrypted at runtime, providing strong security for sensitive credentials.

Validate server hostnames

The TLS implementation validates the server’s hostname using expectedServerHostname:
this.conn = this.conn.startTls({
  expectedServerHostname: this.host,
});
This prevents man-in-the-middle attacks by ensuring the certificate matches the hostname you’re connecting to.
Always use the actual server hostname, not an IP address, when using TLS. Certificates are issued for hostnames, not IP addresses.

Restrict access with authentication

Never use anonymous FTP for production systems:
// INSECURE - Don't do this in production
const client = new FTPClient('ftp.example.com');
// Uses default 'anonymous' / 'anonymous' credentials

Input validation

Validate and sanitize file paths to prevent directory traversal attacks:
function sanitizePath(path: string): string {
  // Remove directory traversal attempts
  const clean = path.replace(/\.\./g, '');
  
  // Remove leading slashes to prevent absolute paths
  return clean.replace(/^\/+/, '');
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const filename = url.searchParams.get('file');
    
    if (!filename) {
      return new Response('Missing file', { status: 400 });
    }
    
    // Sanitize user input
    const safePath = sanitizePath(filename);
    
    const client = new FTPClient(env.FTP_HOST, {
      user: env.FTP_USER,
      pass: env.FTP_PASS,
      secure: true
    });
    
    await client.connect();
    const data = await client.download(safePath);
    await client.close();
    
    return new Response(data);
  }
};
Never pass unsanitized user input directly to FTP methods. Attackers could use ../ sequences to access files outside intended directories.

Rate limiting and abuse prevention

Implement rate limiting to prevent abuse:
import { FTPClient } from 'workerd-ftp';

interface Env {
  FTP_HOST: string;
  FTP_USER: string;
  FTP_PASS: string;
  RATE_LIMITER: RateLimit; // Cloudflare Rate Limiting API
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Rate limit by IP
    const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
    const { success } = await env.RATE_LIMITER.limit({ key: ip });
    
    if (!success) {
      return new Response('Rate limit exceeded', { status: 429 });
    }
    
    // Proceed with FTP operation
    const client = new FTPClient(env.FTP_HOST, {
      user: env.FTP_USER,
      pass: env.FTP_PASS,
      secure: true
    });
    
    await client.connect();
    // ... operations ...
    await client.close();
    
    return new Response('Success');
  }
};

Connection cleanup

Always close connections to prevent resource exhaustion:
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const client = new FTPClient(env.FTP_HOST, {
      user: env.FTP_USER,
      pass: env.FTP_PASS,
      secure: true
    });
    
    try {
      await client.connect();
      
      // Perform operations
      const files = await client.list();
      
      return Response.json({ files });
    } catch (error) {
      console.error('FTP error:', error);
      return new Response('Server error', { status: 500 });
    } finally {
      // Always close, even on error
      await client.close();
    }
  }
};
The close() method from src/classes/ftp-client.ts:543:
public async close(): Promise<void> {
  await this.lock.lock();
  this.conn?.close();
  this.conn = undefined;
  this.dataConn?.close();
  this.dataConn = undefined;
  this.lock.unlock();
}
Use try-finally blocks to ensure connections are closed even if errors occur during operations.

Error handling without leaking information

Don’t expose internal errors to users:
try {
  await client.download('file.txt');
} catch (error: any) {
  // Log detailed error internally
  console.error('FTP download failed:', {
    code: error.code,
    message: error.message,
    timestamp: new Date().toISOString()
  });
  
  // Return generic error to user
  return new Response('File download failed', { status: 500 });
}
Never return raw FTP error messages to users. They may contain sensitive information like server paths, usernames, or internal IP addresses.

Cloudflare Workers security features

Leverage Cloudflare’s security features:

Web Application Firewall (WAF)

Enable WAF rules to protect your Worker endpoints:
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Cloudflare WAF runs before your Worker
    // Configure rules in the dashboard
    
    // Additional custom validation
    if (request.method !== 'GET' && request.method !== 'POST') {
      return new Response('Method not allowed', { status: 405 });
    }
    
    // ... FTP operations ...
  }
};

DDoS protection

Cloudflare’s DDoS protection is automatic, but you should implement application-level protections:
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Check content length
    const contentLength = request.headers.get('Content-Length');
    if (contentLength && parseInt(contentLength) > MAX_FILE_SIZE) {
      return new Response('File too large', { status: 413 });
    }
    
    // ... FTP operations ...
  }
};

IP access control

Restrict access to specific IP addresses:
const ALLOWED_IPS = ['203.0.113.0', '198.51.100.0'];

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const ip = request.headers.get('CF-Connecting-IP');
    
    if (!ip || !ALLOWED_IPS.includes(ip)) {
      return new Response('Forbidden', { status: 403 });
    }
    
    // ... FTP operations ...
  }
};

Audit logging

Log security-relevant events:
interface AuditLog {
  timestamp: string;
  action: string;
  user: string;
  ip: string;
  success: boolean;
  error?: string;
}

async function logAudit(
  env: Env,
  log: AuditLog
): Promise<void> {
  // Send to logging service (e.g., Cloudflare Logs, external SIEM)
  await fetch('https://logs.example.com/audit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(log)
  });
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
    const startTime = Date.now();
    
    try {
      const client = new FTPClient(env.FTP_HOST, {
        user: env.FTP_USER,
        pass: env.FTP_PASS,
        secure: true
      });
      
      await client.connect();
      await client.upload('file.txt', new Uint8Array([1, 2, 3]));
      await client.close();
      
      await logAudit(env, {
        timestamp: new Date().toISOString(),
        action: 'upload',
        user: env.FTP_USER,
        ip,
        success: true
      });
      
      return new Response('Success');
    } catch (error) {
      await logAudit(env, {
        timestamp: new Date().toISOString(),
        action: 'upload',
        user: env.FTP_USER,
        ip,
        success: false,
        error: String(error)
      });
      
      return new Response('Error', { status: 500 });
    }
  }
};

Security checklist

1

Enable TLS

Always set secure: true for production connections.
2

Use secrets

Store credentials in Cloudflare Workers secrets, never in code.
3

Validate inputs

Sanitize all user-provided file paths and parameters.
4

Implement rate limiting

Prevent abuse with rate limiting on FTP operations.
5

Close connections

Always call client.close() in finally blocks.
6

Handle errors safely

Don’t expose internal error details to users.
7

Enable logging

Log security events for audit and monitoring.
8

Use WAF and DDoS protection

Configure Cloudflare’s security features.

Next steps

Connecting

Learn more about connection options

API Reference

Explore the complete API

Build docs developers (and LLMs) love