Secure File Download Implementation: A Complete Guide


Introduction

Implementing secure file downloads is crucial for protecting sensitive data and preventing unauthorized access. This guide covers essential aspects of secure file download implementation, including proper header configuration, rate limiting, access control, and secure file serving techniques.

Content-Disposition Headers

Understanding Content-Disposition

Content-Disposition headers control how files are presented to users:

  • Inline: Display in browser
  • Attachment: Force download
  • Filename handling
  • Security implications

Implementing Secure Headers

const path = require('path');
const mime = require('mime-types');

class SecureDownloadHandler {
  constructor(storageDir) {
    this.storageDir = storageDir;
  }
  
  sanitizeFilename(filename) {
    // Remove path traversal characters and invalid chars
    return filename.replace(/[^a-zA-Z0-9._-]/g, '_');
  }
  
  getContentType(filename) {
    // Get MIME type or default to octet-stream
    return mime.lookup(filename) || 'application/octet-stream';
  }
  
  setSecureHeaders(res, filename, contentType) {
    const sanitizedName = this.sanitizeFilename(filename);
    
    res.setHeader('Content-Type', contentType);
    res.setHeader('Content-Disposition', `attachment; filename="${sanitizedName}"`);
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate');
    res.setHeader('Pragma', 'no-cache');
    res.setHeader('Expires', '0');
  }
}

// Example usage
const downloadHandler = new SecureDownloadHandler(STORAGE_DIR);

app.get('/download/:fileId', async (req, res) => {
  try {
    const fileId = req.params.fileId;
    const fileMetadata = await getFileMetadata(fileId);
    
    if (!fileMetadata) {
      return res.status(404).json({ error: 'File not found' });
    }
    
    const filePath = path.join(STORAGE_DIR, fileMetadata.storedName);
    const contentType = downloadHandler.getContentType(fileMetadata.originalName);
    
    // Set secure headers
    downloadHandler.setSecureHeaders(res, fileMetadata.originalName, contentType);
    
    // Stream file
    res.sendFile(filePath);
  } catch (error) {
    console.error('Download error:', error);
    res.status(500).json({ error: 'Download failed' });
  }
});

Header Best Practices

  1. Content-Type handling:

    • Use correct MIME types
    • Prevent MIME sniffing
    • Handle unknown types safely
  2. Filename security:

    • Sanitize filenames
    • Handle special characters
    • Prevent injection attacks
  3. Cache control:

    • Prevent caching sensitive files
    • Use appropriate directives
    • Consider privacy implications

Download Rate Limiting

Implementing Rate Limits

Rate limiting prevents:

  • Resource exhaustion
  • Bandwidth abuse
  • DoS attacks
  • Scraping attempts

Rate Limiting Implementation

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

class DownloadRateLimiter {
  constructor() {
    this.redis = new Redis({
      host: process.env.REDIS_HOST,
      port: process.env.REDIS_PORT,
      password: process.env.REDIS_PASSWORD
    });
    
    this.limiter = rateLimit({
      store: new RedisStore({
        client: this.redis,
        prefix: 'download_limit:'
      }),
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Limit each IP to 100 downloads per window
      message: { error: 'Too many download requests' }
    });
    
    this.fileSizeLimiter = this.createFileSizeLimiter();
  }
  
  createFileSizeLimiter() {
    return async (req, res, next) => {
      try {
        const fileId = req.params.fileId;
        const fileMetadata = await getFileMetadata(fileId);
        
        if (!fileMetadata) {
          return res.status(404).json({ error: 'File not found' });
        }
        
        // Check user's download quota
        const userQuota = await this.getUserQuota(req.user.id);
        
        if (fileMetadata.size > userQuota.remaining) {
          return res.status(429).json({ 
            error: 'Download quota exceeded' 
          });
        }
        
        // Attach metadata for later use
        req.fileMetadata = fileMetadata;
        next();
      } catch (error) {
        next(error);
      }
    };
  }
  
  async getUserQuota(userId) {
    const key = `user_quota:${userId}`;
    const quota = await this.redis.get(key);
    
    return JSON.parse(quota) || {
      limit: 1024 * 1024 * 1024, // 1GB
      used: 0,
      remaining: 1024 * 1024 * 1024
    };
  }
  
  async updateQuota(userId, bytes) {
    const key = `user_quota:${userId}`;
    const quota = await this.getUserQuota(userId);
    
    quota.used += bytes;
    quota.remaining -= bytes;
    
    await this.redis.set(key, JSON.stringify(quota), 'EX', 86400); // 24h
  }
}

// Example usage
const rateLimiter = new DownloadRateLimiter();

app.get('/download/:fileId',
  rateLimiter.limiter,
  rateLimiter.fileSizeLimiter,
  async (req, res) => {
    try {
      // Download logic here...
      
      // Update quota after successful download
      await rateLimiter.updateQuota(
        req.user.id, 
        req.fileMetadata.size
      );
    } catch (error) {
      console.error('Download error:', error);
      res.status(500).json({ error: 'Download failed' });
    }
  }
);

Rate Limiting Best Practices

  1. Configure appropriate limits:

    • Consider user roles
    • Adjust for file sizes
    • Monitor usage patterns
  2. Implement quotas:

    • Daily/monthly limits
    • Size-based quotas
    • User-specific limits
  3. Handle limit violations:

    • Clear error messages
    • Retry-After headers
    • Monitoring and alerts

Access Control

Implementing Download Authorization

const jwt = require('jsonwebtoken');

class DownloadAuthorization {
  constructor(secret) {
    this.secret = secret;
  }
  
  async generateDownloadToken(userId, fileId, expiresIn = '1h') {
    return jwt.sign(
      { userId, fileId, type: 'download' },
      this.secret,
      { expiresIn }
    );
  }
  
  verifyToken(token) {
    try {
      return jwt.verify(token, this.secret);
    } catch (error) {
      return null;
    }
  }
  
  async authorizeDownload(req, res, next) {
    try {
      const fileId = req.params.fileId;
      const token = req.query.token;
      
      // Verify download token
      const decoded = this.verifyToken(token);
      
      if (!decoded || decoded.fileId !== fileId) {
        return res.status(403).json({ error: 'Invalid download token' });
      }
      
      // Check file permissions
      const hasAccess = await this.checkFileAccess(
        decoded.userId,
        fileId
      );
      
      if (!hasAccess) {
        return res.status(403).json({ error: 'Access denied' });
      }
      
      // Log download attempt
      await this.logDownloadAttempt(decoded.userId, fileId);
      
      next();
    } catch (error) {
      next(error);
    }
  }
  
  async checkFileAccess(userId, fileId) {
    // Implement your permission checking logic
    const permissions = await getFilePermissions(userId, fileId);
    return permissions.canDownload;
  }
  
  async logDownloadAttempt(userId, fileId) {
    await saveAuditLog({
      type: 'download_attempt',
      userId,
      fileId,
      timestamp: new Date(),
      ip: req.ip
    });
  }
}

// Example usage
const auth = new DownloadAuthorization(process.env.JWT_SECRET);

app.get('/download/:fileId',
  auth.authorizeDownload.bind(auth),
  async (req, res) => {
    // Download logic here...
  }
);

// Generate download link
app.post('/files/:fileId/download-link', async (req, res) => {
  try {
    const fileId = req.params.fileId;
    const userId = req.user.id;
    
    // Generate temporary download token
    const token = await auth.generateDownloadToken(userId, fileId);
    
    // Create download URL
    const downloadUrl = `${process.env.API_URL}/download/${fileId}?token=${token}`;
    
    res.json({ downloadUrl });
  } catch (error) {
    console.error('Error generating download link:', error);
    res.status(500).json({ error: 'Failed to generate download link' });
  }
});

Access Control Best Practices

  1. Token-based access:

    • Short-lived tokens
    • Single-use tokens
    • Proper token validation
  2. Permission checking:

    • Role-based access
    • Resource ownership
    • Group permissions
  3. Audit logging:

    • Track all attempts
    • Log access patterns
    • Alert on anomalies

Secure File Serving

Implementing Secure Downloads

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

class SecureFileServer {
  constructor(storageDir) {
    this.storageDir = storageDir;
  }
  
  async streamFile(filePath, res) {
    const stream = fs.createReadStream(filePath);
    
    // Handle stream errors
    stream.on('error', (error) => {
      console.error('Stream error:', error);
      res.status(500).end();
    });
    
    // Pipe file to response
    stream.pipe(res);
  }
  
  async streamLargeFile(filePath, req, res) {
    const stat = await fs.promises.stat(filePath);
    
    // Handle range requests
    const range = req.headers.range;
    
    if (range) {
      const parts = range.replace(/bytes=/, '').split('-');
      const start = parseInt(parts[0], 10);
      const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
      const chunkSize = end - start + 1;
      
      res.writeHead(206, {
        'Content-Range': `bytes ${start}-${end}/${stat.size}`,
        'Accept-Ranges': 'bytes',
        'Content-Length': chunkSize,
        'Content-Type': req.fileMetadata.contentType
      });
      
      const stream = fs.createReadStream(filePath, { start, end });
      stream.pipe(res);
    } else {
      res.writeHead(200, {
        'Content-Length': stat.size,
        'Content-Type': req.fileMetadata.contentType
      });
      
      const stream = fs.createReadStream(filePath);
      stream.pipe(res);
    }
  }
  
  async verifyFileIntegrity(filePath, expectedHash) {
    return new Promise((resolve, reject) => {
      const hash = crypto.createHash('sha256');
      const stream = fs.createReadStream(filePath);
      
      stream.on('data', data => hash.update(data));
      stream.on('end', () => {
        const fileHash = hash.digest('hex');
        resolve(fileHash === expectedHash);
      });
      stream.on('error', reject);
    });
  }
}

// Example usage
const fileServer = new SecureFileServer(STORAGE_DIR);

app.get('/download/:fileId', async (req, res) => {
  try {
    const fileId = req.params.fileId;
    const fileMetadata = await getFileMetadata(fileId);
    
    if (!fileMetadata) {
      return res.status(404).json({ error: 'File not found' });
    }
    
    const filePath = path.join(STORAGE_DIR, fileMetadata.storedName);
    
    // Verify file integrity
    const isValid = await fileServer.verifyFileIntegrity(
      filePath,
      fileMetadata.hash
    );
    
    if (!isValid) {
      return res.status(500).json({ error: 'File integrity check failed' });
    }
    
    // Stream file based on size
    if (fileMetadata.size > 100 * 1024 * 1024) { // 100MB
      await fileServer.streamLargeFile(filePath, req, res);
    } else {
      await fileServer.streamFile(filePath, res);
    }
  } catch (error) {
    console.error('Download error:', error);
    res.status(500).json({ error: 'Download failed' });
  }
});

File Serving Best Practices

  1. Streaming implementation:

    • Handle large files
    • Support range requests
    • Manage memory usage
  2. Error handling:

    • Graceful error recovery
    • Clean stream termination
    • Resource cleanup
  3. Performance optimization:

    • Compression options
    • Caching strategies
    • Load balancing

Conclusion

Implementing secure file downloads requires careful attention to multiple security aspects. By properly implementing content headers, rate limiting, access control, and secure file serving, you create a robust and secure download system.

Key takeaways:

  • Use appropriate content headers
  • Implement rate limiting
  • Enforce access control
  • Stream files securely
  • Monitor download activity

Additional Resources