WhatsApp API Security: Best Practices

Panduan keamanan WhatsApp API. Protect credentials, secure webhook, prevent abuse. Security best practices untuk developer!

WhatsApp API Security
WhatsApp API Security

Security bukan optional!

WhatsApp bot handle data customer. Satu breach bisa hancurkan trust dan bisnis.


Security Layers

┌─────────────────────────────────────┐
│         APPLICATION LAYER           │
│    Input validation, rate limit     │
├─────────────────────────────────────┤
│          API LAYER                  │
│   Auth, webhook verification        │
├─────────────────────────────────────┤
│         NETWORK LAYER               │
│      HTTPS, firewall, VPN           │
├─────────────────────────────────────┤
│        INFRASTRUCTURE               │
│   Server hardening, updates         │
└─────────────────────────────────────┘

1. Protect Credentials

Never Hardcode:

javascript

// ❌ NEVER DO THIS
const ACCESS_TOKEN = 'EAABx123...';
const API_KEY = 'sk-abc123...';

// ✅ USE ENVIRONMENT VARIABLES
require('dotenv').config();
const ACCESS_TOKEN = process.env.WHATSAPP_ACCESS_TOKEN;
const API_KEY = process.env.OPENAI_API_KEY;

.env File:

env

# .env (NEVER commit to git!)
WHATSAPP_ACCESS_TOKEN=EAABx123...
WHATSAPP_PHONE_ID=1234567890
OPENAI_API_KEY=sk-abc123...
WEBHOOK_VERIFY_TOKEN=my_secret_token

.gitignore:

gitignore

.env
.env.local
auth_info/
*.pem
*.key
node_modules/

2. Secure Webhook

Verify Signature:

javascript

const crypto = require('crypto');

function verifyWebhookSignature(req, appSecret) {
    const signature = req.headers['x-hub-signature-256'];
    
    if (!signature) {
        return false;
    }
    
    const payload = JSON.stringify(req.body);
    const expectedSignature = 'sha256=' + 
        crypto.createHmac('sha256', appSecret)
            .update(payload)
            .digest('hex');
    
    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
    );
}

// Middleware
app.post('/webhook', (req, res) => {
    if (!verifyWebhookSignature(req, process.env.APP_SECRET)) {
        console.log('Invalid webhook signature');
        return res.sendStatus(401);
    }
    
    // Process verified webhook
    processWebhook(req.body);
    res.sendStatus(200);
});

Verify Token:

javascript

app.get('/webhook', (req, res) => {
    const mode = req.query['hub.mode'];
    const token = req.query['hub.verify_token'];
    const challenge = req.query['hub.challenge'];
    
    // Use constant-time comparison
    const expectedToken = process.env.WEBHOOK_VERIFY_TOKEN;
    const tokenValid = crypto.timingSafeEqual(
        Buffer.from(token || ''),
        Buffer.from(expectedToken)
    );
    
    if (mode === 'subscribe' && tokenValid) {
        res.status(200).send(challenge);
    } else {
        res.sendStatus(403);
    }
});

3. Input Validation

Sanitize User Input:

javascript

const validator = require('validator');

function sanitizeMessage(text) {
    if (!text || typeof text !== 'string') {
        return '';
    }
    
    // Remove potential injection
    let sanitized = text
        .replace(/<[^>]*>/g, '') // Remove HTML tags
        .replace(/javascript:/gi, '') // Remove JS protocol
        .trim();
    
    // Limit length
    if (sanitized.length > 4096) {
        sanitized = sanitized.substring(0, 4096);
    }
    
    return sanitized;
}

// Validate phone number
function validatePhone(phone) {
    // Indonesian format
    const phoneRegex = /^62[0-9]{9,13}$/;
    return phoneRegex.test(phone);
}

Prevent Command Injection:

javascript

// ❌ DANGEROUS
const userInput = message.text;
exec(`echo ${userInput}`); // Shell injection risk!

// ✅ SAFE
const userInput = sanitizeMessage(message.text);
// Never pass user input to shell commands

4. Rate Limiting

javascript

const rateLimit = require('express-rate-limit');

// Global rate limit
const globalLimiter = rateLimit({
    windowMs: 60 * 1000, // 1 minute
    max: 100, // 100 requests per minute
    message: 'Too many requests'
});

// Webhook specific limit
const webhookLimiter = rateLimit({
    windowMs: 1000, // 1 second
    max: 50 // 50 per second (high for webhooks)
});

app.use('/api', globalLimiter);
app.use('/webhook', webhookLimiter);

Per-User Rate Limit:

javascript

const userLimits = new Map();

function checkUserLimit(userId) {
    const now = Date.now();
    const userHistory = userLimits.get(userId) || [];
    
    // Remove old entries (older than 1 minute)
    const recent = userHistory.filter(t => now - t < 60000);
    
    if (recent.length >= 20) { // Max 20 messages/minute
        return false;
    }
    
    recent.push(now);
    userLimits.set(userId, recent);
    return true;
}

5. Data Protection

Encrypt Sensitive Data:

javascript

const crypto = require('crypto');

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; // 32 bytes
const IV_LENGTH = 16;

function encrypt(text) {
    const iv = crypto.randomBytes(IV_LENGTH);
    const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
    let encrypted = cipher.update(text);
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    return iv.toString('hex') + ':' + encrypted.toString('hex');
}

function decrypt(text) {
    const parts = text.split(':');
    const iv = Buffer.from(parts.shift(), 'hex');
    const encrypted = Buffer.from(parts.join(':'), 'hex');
    const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
    let decrypted = decipher.update(encrypted);
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return decrypted.toString();
}

Mask Sensitive Info in Logs:

javascript

function maskPhone(phone) {
    if (!phone) return '';
    return phone.slice(0, 4) + '****' + phone.slice(-4);
}

function maskData(data) {
    return {
        ...data,
        phone: maskPhone(data.phone),
        email: data.email?.replace(/(.{2})(.*)(@.*)/, '$1***$3')
    };
}

// Logging
console.log('Processing message from:', maskPhone(message.from));

6. Auth Session Security

javascript

// Store auth securely
const authPath = process.env.AUTH_PATH || './auth_info';

// Set proper permissions
const fs = require('fs');
fs.chmodSync(authPath, 0o700); // Owner only

// Encrypt auth files at rest (optional but recommended)
async function saveAuthEncrypted(state) {
    const encrypted = encrypt(JSON.stringify(state));
    fs.writeFileSync(`${authPath}/creds.enc`, encrypted);
}

7. Server Hardening

HTTPS Only:

javascript

const https = require('https');
const fs = require('fs');

const options = {
    key: fs.readFileSync('private.key'),
    cert: fs.readFileSync('certificate.crt')
};

https.createServer(options, app).listen(443);

// Redirect HTTP to HTTPS
app.use((req, res, next) => {
    if (!req.secure) {
        return res.redirect('https://' + req.headers.host + req.url);
    }
    next();
});

Security Headers:

javascript

const helmet = require('helmet');

app.use(helmet());
app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"]
    }
}));

Security Checklist

□ Credentials in environment variables
□ .env in .gitignore
□ Webhook signature verification
□ Input validation & sanitization
□ Rate limiting implemented
□ HTTPS only
□ Security headers (helmet)
□ Auth files protected
□ Sensitive data encrypted
□ Logs sanitized (no PII in plain text)
□ Regular dependency updates
□ Error messages don't leak info

FAQ

Bagaimana jika credentials bocor?

Immediate action: Rotate semua credentials, review access logs, notify affected users.

Perlu penetration testing?

Recommended untuk production. Minimal gunakan tools seperti OWASP ZAP untuk basic scan.

Bagaimana comply dengan data protection laws?

Implement: consent collection, data minimization, right to deletion, encryption at rest.


Kesimpulan

Security = Foundation, bukan afterthought!

LayerProtection
CredentialsEnv vars, never hardcode
WebhookSignature verification
InputValidation & sanitization
DataEncryption & masking
ServerHTTPS, headers, hardening

Secure by design!

Build Secure Bot →


Artikel Terkait