
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
-
Content-Type handling:
- Use correct MIME types
- Prevent MIME sniffing
- Handle unknown types safely
-
Filename security:
- Sanitize filenames
- Handle special characters
- Prevent injection attacks
-
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
-
Configure appropriate limits:
- Consider user roles
- Adjust for file sizes
- Monitor usage patterns
-
Implement quotas:
- Daily/monthly limits
- Size-based quotas
- User-specific limits
-
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
-
Token-based access:
- Short-lived tokens
- Single-use tokens
- Proper token validation
-
Permission checking:
- Role-based access
- Resource ownership
- Group permissions
-
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
-
Streaming implementation:
- Handle large files
- Support range requests
- Manage memory usage
-
Error handling:
- Graceful error recovery
- Clean stream termination
- Resource cleanup
-
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