Introduction

Welcome to the final lesson in our Server-Side Request Forgery (SSRF) Prevention in Express course! In this lesson, we'll explore security logging and monitoring in depth. Effective logging and monitoring are crucial components of a comprehensive security strategy, as they help you detect, investigate, and respond to security incidents promptly. Let's dive in and discover how to implement these practices in your Express applications! 📊

The Role of Security Logging

Security logging is the practice of recording events related to security concerns within your application. Properly implemented logs serve multiple purposes:

  1. Detecting Security Incidents: Logs can reveal suspicious activities that may indicate ongoing attacks.
  2. Investigating Breaches: After a security incident, logs provide valuable data for forensic analysis.
  3. Compliance Requirements: Many regulatory frameworks require specific logging practices.
  4. System Auditing: Logs help track user activities and system changes over time.

Let's implement a comprehensive logging system using the popular Winston library:

import express from 'express';
import winston from 'winston';
import expressWinston from 'express-winston';
import path from 'path';

const app = express();
app.use(express.json());

// Configure logging format and transports
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  defaultMeta: { service: 'web-service' },
  transports: [
    // Console transport for development
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),
    // File transport for persistent logs
    new winston.transports.File({ 
      filename: path.join(__dirname, 'logs/error.log'), 
      level: 'error' 
    }),
    new winston.transports.File({ 
      filename: path.join(__dirname, 'logs/combined.log')
    })
  ]
});

// Log all HTTP requests
app.use(expressWinston.logger({
  winstonInstance: logger,
  meta: true,
  msg: 'HTTP {{req.method}} {{req.url}}',
  expressFormat: true,
  colorize: false
}));

// Add custom security logging middleware
app.use((req, res, next) => {
  // Log potentially suspicious activities
  if (req.body?.url || req.query?.url) {
    logger.warn('URL parameter detected', {
      ip: req.ip,
      url: req.body.url || req.query.url,
      method: req.method,
      path: req.path,
      userAgent: req.headers['user-agent']
    });
  }
  next();
});

// Example route
app.get('/', (req, res) => {
  res.send('Hello, world!');
});

// Error logging middleware
app.use(expressWinston.errorLogger({
  winstonInstance: logger
}));

app.listen(3000, () => {
  logger.info('Server running on port 3000');
});

This code sets up a robust logging system using Winston. It logs all HTTP requests and errors to both the console and files, making it easier to monitor and analyze application activities.

Structured Logging for Security Events

To make security logs more useful, it's important to use structured logging with consistent fields:

// Define security event types for consistency
enum SecurityEventType {
  AUTHENTICATION_FAILURE = 'authentication_failure',
  AUTHORIZATION_FAILURE = 'authorization_failure',
  INPUT_VALIDATION_FAILURE = 'input_validation_failure',
  POTENTIAL_SSRF_ATTEMPT = 'potential_ssrf_attempt',
  SENSITIVE_DATA_ACCESS = 'sensitive_data_access'
}

// Function to log security events with consistent structure
function logSecurityEvent(
  logger: winston.Logger, 
  eventType: SecurityEventType, 
  message: string, 
  metadata: Record<string, any>
) {
  logger.warn(message, {
    security_event_type: eventType,
    timestamp: new Date().toISOString(),
    ...metadata
  });
}

// Example usage in a route
app.post('/fetch-url', (req, res) => {
  const { url } = req.body;
  
  if (!url || typeof url !== 'string') {
    logSecurityEvent(logger, SecurityEventType.INPUT_VALIDATION_FAILURE, 'Invalid URL input', {
      ip: req.ip,
      input: url,
      user_id: req.user?.id, // Assuming you have user info in the request
      path: req.path
    });
    
    return res.status(400).json({ error: 'Invalid URL' });
  }
  
  // Check for potential SSRF attempt
  if (isSuspiciousUrl(url)) {
    logSecurityEvent(logger, SecurityEventType.POTENTIAL_SSRF_ATTEMPT, 'Potential SSRF attempt detected', {
      ip: req.ip,
      url: url,
      user_id: req.user?.id,
      user_agent: req.headers['user-agent']
    });
    
    return res.status(403).json({ error: 'Access denied' });
  }
  
  // Process the URL (securely)
  res.json({ success: true });
});

function isSuspiciousUrl(url: string): boolean {
  // Implement your SSRF detection logic here
  const suspiciousPatterns = ['localhost', '127.0.0.1', 'internal', '192.168.'];
  return suspiciousPatterns.some(pattern => url.includes(pattern));
}

This approach ensures that security events are logged with consistent fields, making it easier to analyze and correlate events across your application.

Real-time Log Monitoring

Logging is only effective if someone is monitoring the logs. Let's implement a simple real-time monitoring system:

import express from 'express';
import http from 'http';
import { Server as SocketServer } from 'socket.io';
import winston from 'winston';

const app = express();
const server = http.createServer(app);
const io = new SocketServer(server);

// Configure Winston with a custom transport for Socket.io
class SocketTransport extends winston.Transport {
  constructor(opts?: winston.transport.TransportStreamOptions) {
    super(opts);
  }

  log(info: any, callback: () => void) {
    setImmediate(() => {
      this.emit('logged', info);
    });

    // Broadcast log to all connected clients
    io.emit('log-event', info);

    callback();
  }
}

// Create logger with Socket.io transport
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'combined.log' }),
    new SocketTransport()
  ]
});

// Socket.io connection handling
io.on('connection', (socket) => {
  console.log('Admin connected to log monitoring');
  
  socket.on('disconnect', () => {
    console.log('Admin disconnected from log monitoring');
  });
});

// Example route with security logging
app.get('/api/data', (req, res) => {
  logger.info('Data accessed', { ip: req.ip, path: req.path });
  res.json({ data: 'sensitive information' });
});

// Example route that triggers a security warning
app.post('/api/fetch', (req, res) => {
  const { url } = req.body;
  
  if (url && isSuspiciousUrl(url)) {
    logger.warn('Potential SSRF attempt', { 
      ip: req.ip,
      url,
      userAgent: req.headers['user-agent']
    });
    return res.status(403).json({ error: 'Access denied' });
  }
  
  res.json({ success: true });
});

function isSuspiciousUrl(url: string): boolean {
  // Implement SSRF detection logic
  return url.includes('internal') || url.includes('localhost');
}

// Simple admin dashboard to view logs in real-time
app.get('/admin/logs', (req, res) => {
  // In a real app, this would require authentication
  res.send(`
    <html>
      <head>
        <title>Security Log Monitor</title>
        <script src="/socket.io/socket.io.js"></script>
        <script>
          const socket = io();
          
          socket.on('log-event', (log) => {
            const logElement = document.createElement('div');
            logElement.textContent = JSON.stringify(log);
            logElement.className = log.level;
            document.getElementById('logs').prepend(logElement);
          });
        </script>
        <style>
          .warn, .error { color: red; font-weight: bold; }
          .info { color: blue; }
          #logs { max-height: 500px; overflow-y: auto; }
        </style>
      </head>
      <body>
        <h1>Security Log Monitor</h1>
        <div id="logs"></div>
      </body>
    </html>
  `);
});

server.listen(3000, () => {
  logger.info('Server running on port 3000');
});

This implementation uses Socket.io to broadcast log events to a simple admin dashboard, allowing for real-time monitoring of security events.

Setting Up Alerts Based on Log Patterns
Conclusion

In this lesson, we explored the importance of security logging and monitoring in protecting Express applications from SSRF and other attacks. We learned how to implement structured logging, real-time monitoring, and automated alerting based on log patterns. By integrating these practices into your security strategy, you can significantly enhance your ability to detect and respond to security incidents.

Throughout this course, we've covered the fundamentals of SSRF, prevention techniques in Express, incident response, and comprehensive monitoring. These skills will help you build more secure applications and protect your users' data from potential threats.

Remember that security is an ongoing process, not a one-time implementation. Continue to stay informed about emerging threats and best practices to ensure your applications remain secure in an ever-evolving landscape. Thank you for joining us on this journey to better security! 🚀

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal