Your app works, fantastic! But putting it online without security is like opening a restaurant without locks on the doors. This appendix guides you through ALL the security checks needed before going live. From hidden API keys (you’ve seen them!) to XSS attacks (what’s that? you’ll find out!), through secure passwords and backups. Don’t panic: with the right precautions, you’ll sleep soundly while your app conquers the world!
Until now you’ve been cooking for trusted friends (localhost). Now you’re about to open to the public, and not all customers are honest!
Imagine:
Your restaurant (app) needs:
Cross-Site Scripting (XSS) = When someone injects malicious JavaScript code into your app.
It’s like a customer writing on the restaurant menu: “Free meal if you give your credit card!” and the next customers falling for it.
Attack example:
// A malicious user enters this in the "name" field:
<script>alert('Gotcha!')</script>;
// If you don't sanitize, when you display:
document.getElementById("welcome").innerHTML = `Hello ${name}`;
// The browser executes the script!
Defense: Sanitization
// ❌ DANGEROUS
element.innerHTML = userInput;
// ✅ SAFE
element.textContent = userInput; // Treats everything as text, not code
// ✅ Or use a library like DOMPurify
element.innerHTML = DOMPurify.sanitize(userInput);
Remember from Module 8? It’s when someone turns an innocent query into a dangerous command.
Example:
// ❌ DANGEROUS
const query = `SELECT * FROM users WHERE name = '${userInput}'`;
// If userInput = "'; DROP TABLE users; --"
// Goodbye database!
// ✅ SAFE - Use prepared statements
const query = "SELECT * FROM users WHERE name = ?";
db.query(query, [userInput]); // Database knows it's a value, not a command
Cross-Site Request Forgery = When a malicious site makes your app perform actions on behalf of a logged-in user.
It’s like someone forging a restaurant order with your signature.
Defense: CSRF Tokens
A CSRF token is a unique secret code generated for each form/session. It verifies that the request really comes from YOUR site and not from an external site trying to “impersonate” the user. Every important form must have one!
// Generate a unique token for each form
const csrfToken = generateRandomToken();
// In the HTML form
<input type="hidden" name="csrf_token" value="${csrfToken}">
// When receiving the form, verify
if (request.csrfToken !== session.csrfToken) {
throw new Error('Nice try, hacker!');
}
Brute force bots try thousands of password combinations per second until they guess right. Without protection, “password123” is discovered in milliseconds! Rate limiting is like putting a timer on the door: after X attempts, you have to wait.
Defense: Smart Rate Limiting
// Simple rate limiter
const attempts = {};
function login(email, password) {
// Blocked?
if (attempts[email]?.blockedUntil > Date.now()) {
return "Account blocked. Try again later.";
}
if (!correctPassword) {
attempts[email] = (attempts[email]?.count || 0) + 1;
if (attempts[email] >= 5) {
attempts[email].blockedUntil = Date.now() + 15 * 60 * 1000;
return "Too many attempts. Blocked for 15 minutes.";
}
}
}
// In production use express-rate-limit
// ❌ NEVER in frontend (everyone sees)
const API_KEY = "sk_super_secret_123";
// ✅ In backend or environment variables
const API_KEY = process.env.API_KEY;
// ✅ Or use a backend proxy
// Frontend → Your Backend → External API
// The key stays in your backend, invisible!
NEVER save passwords in plain text!
// ❌ DISASTER
const password = "123456"; // Visible in database!
// ✅ USE HASHING (with libraries like bcrypt)
const bcrypt = require("bcrypt");
const hashedPassword = await bcrypt.hash(password, 10);
// Save: $2b$10$EixZaYVK1fsbw1ZfbX3OXe.jxPLQ7... (irreversible!)
// To verify:
const match = await bcrypt.compare(enteredPassword, hashedPassword);
HTTP = Talking by shouting in the square (everyone hears)
HTTPS = Talking in secret code (only you and the server understand)
How to enable it:
NEVER trust what comes from the frontend! It’s like having quality control: even if the product looks perfect, you always check before putting it on sale.
// Frontend says: "Age: 25"
// But a hacker could send: "Age: -999" or "Age: DROP TABLE"
// ✅ ALWAYS validate in the backend
function validateAge(age) {
const ageNum = parseInt(age);
if (isNaN(ageNum) || ageNum < 0 || ageNum > 150) {
throw new Error("Invalid age");
}
return ageNum;
}
// For email
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(email)) {
throw new Error("Invalid email");
}
return email.toLowerCase();
}
CSP tells the browser what can and cannot execute on your page. It’s like having strict rules about what can enter your home: if it’s not on the guest list, it stays out!
<!-- In your HTML -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';"
/>
Translation:
default-src 'self' = Load resources only from my domainscript-src = Scripts only from my site or trusted-cdn.comstyle-src = Styles from my site + inline CSS (for convenience)This automatically blocks scripts injected by XSS attackers!
Why is it dangerous? An attacker could upload a .php file disguised as .jpg. If your server executes it instead of displaying it… game over! Or they could upload huge files to crash the server, or malicious scripts that other users download.
// ✅ ESSENTIAL CHECKS
function validateUpload(file) {
// 1. Maximum size (prevents DoS)
if (file.size > 5 * 1024 * 1024) {
// 5MB
throw new Error("File too large");
}
// 2. File type - check MIME type AND extension
const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
if (!allowedTypes.includes(file.mimetype)) {
throw new Error("File type not allowed");
}
// 3. Check "magic bytes" (first bytes of file)
// A real JPEG starts with FF D8 FF
const magicBytes = file.buffer.slice(0, 3);
// 4. ALWAYS rename (prevents path traversal attacks)
const newName = `${Date.now()}_${crypto.randomUUID()}.jpg`;
// 5. NEVER save where code is executable
// Save in /uploads, not in /public or /src
// 6. Better yet: use external CDNs
// Cloudinary, S3, etc. - they handle security
}
// CORRECT FLOW:
// 1. User sends email + password
// 2. Verify password with hash
// 3. Create session token
// 4. Save token in httpOnly cookie
// ✅ Secure cookie
res.cookie("session", token, {
httpOnly: true, // JavaScript can't read it
secure: true, // HTTPS only
sameSite: "strict", // Prevents CSRF
maxAge: 3600000, // Expires after 1 hour
});
// ❌ NEVER save in localStorage (JavaScript can read it!)
JWT (JSON Web Token) is a token containing user information in JSON format, digitally signed. It’s like a festival wristband: proves you paid (authenticated), contains info about you (name, ticket type), and can’t be forged (digital signature).
The beauty? The server doesn’t need to remember anything! Each request carries all necessary info.
// A JWT has 3 parts separated by dots:
// header.payload.signature
// eyJhbGc.eyJ1c2VyIjo.SflKxwRJ
const jwt = require("jsonwebtoken");
// Create token after successful login
const token = jwt.sign(
{
userId: 123,
email: "user@example.com",
role: "user", // User info in token
},
process.env.JWT_SECRET, // Secret key for signing
{ expiresIn: "1h" } // Expires after 1 hour
);
// Verify token on each request
function verifyToken(req, res, next) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Now you know who the user is!
next();
} catch {
res.status(401).send("Invalid or expired token");
}
}
Advantages: Stateless (server doesn’t keep sessions), scalable, works with APIs Caution: Once issued can’t be revoked (until expiration), so keep times short!
Remember from Module 7? CORS (Cross-Origin Resource Sharing) decides who can call your APIs. It’s the bouncer checking: “Where are you from? Are you on the list?” If the frontend is on mysite.com and the API on api.mysite.com, without CORS the browser blocks everything!
// In your backend
app.use(
cors({
origin: "https://yoursite.com", // Only your frontend
credentials: true, // Allow cookies
})
);
// Or multiple origins
const allowedOrigins = ["https://yoursite.com", "http://localhost:3000"];
app.use(
cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("CORS not allowed"));
}
},
})
);
// Never do this in production!
app.use(cors({ origin: "*" })); // Anyone can call!
Security Headers are HTTP headers that tell the browser how to behave. They’re like invisible security signs protecting your users even if they don’t see them. Each header blocks a specific type of attack!
// Add these headers to responses
app.use((req, res, next) => {
// X-Frame-Options: Prevents clickjacking
// (hiding your site in an iframe to steal clicks)
res.setHeader("X-Frame-Options", "DENY");
// X-Content-Type-Options: Prevents MIME type sniffing
// (browser shouldn't "guess" that a .txt is a .js)
res.setHeader("X-Content-Type-Options", "nosniff");
// X-XSS-Protection: Activates browser XSS protection
// (Chrome/Edge have built-in anti-XSS filters)
res.setHeader("X-XSS-Protection", "1; mode=block");
// Strict-Transport-Security: Force HTTPS forever
// (even if user types http://, goes to https://)
res.setHeader(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains"
);
// Referrer-Policy: Control info sent to external sites
// (don't reveal full URLs with tokens or sensitive info)
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
next();
});
Pro tip: Use securityheaders.com to test your headers!
// Log EVERYTHING that matters
function logSecurity(event, details) {
const log = {
timestamp: new Date().toISOString(),
event: event,
ip: details.ip,
userAgent: details.userAgent,
userId: details.userId || "anonymous",
};
console.log("[SECURITY]", JSON.stringify(log));
// In production: send to service like Sentry or LogRocket
}
// Examples of what to log:
logSecurity("LOGIN_FAILED", { email, ip });
logSecurity("FILE_UPLOAD", { filename, size, type });
logSecurity("API_RATE_LIMIT", { endpoint, ip });
An untested backup is a backup that doesn’t exist! There’s a 3-2-1 rule:
// Basic automatic backup
const cron = require("node-cron");
// Every night at 2:00 AM
cron.schedule("0 2 * * *", async () => {
// 1. Database dump
await backupDatabase();
// 2. Upload to cloud
await uploadToS3();
// 3. Verify it works!
await testBackup();
// 4. Delete old (keep last 30)
await cleanOldBackups();
});
Privacy laws protect users worldwide. Different regions have different requirements:
// 1. EXPLICIT CONSENT - Never pre-checked!
<input type="checkbox" id="privacy" required>
<label>I accept the Privacy Policy</label>
// 2. RIGHT TO DELETION - Complete removal
app.delete('/api/delete-my-data', async (req, res) => {
await deleteAllUserData(req.user.id);
res.send('All data deleted');
});
// 3. DATA PORTABILITY - Export data
app.get('/api/export-my-data', async (req, res) => {
const userData = await getAllUserData(req.user.id);
res.json(userData); // Downloadable JSON
});
// 4. COOKIE CONSENT if using analytics/ads
if (!hasConsent()) showCookieBanner();
Golden rules: Only collect necessary data, explain why, protect everything, delete when no longer needed. Check your local regulations!
Environment variables are like a locked drawer where you keep your house keys. They live on the server (not in code) and contain all secrets: database passwords, API keys, sensitive configurations. The .env file is the local version for development.
# .env (for local development)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=change_this_super_secret_string_in_production_abc123xyz
STRIPE_SECRET_KEY=sk_test_abcdefghijklmnop
SMTP_PASSWORD=secret_email_password
OPENAI_API_KEY=sk-proj-abc123def456
# CRITICAL: Add .env to .gitignore!
# Otherwise it ends up on GitHub for everyone!
// How to use them in code
require("dotenv").config(); // Load .env file
// Access variables
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
// Verify they're all there (fail fast!)
const requiredEnv = ["DATABASE_URL", "JWT_SECRET", "STRIPE_SECRET_KEY"];
requiredEnv.forEach((key) => {
if (!process.env[key]) {
console.error(`Missing environment variable: ${key}`);
process.exit(1); // Stop everything, better than crashing later
}
});
// In production (Heroku, Vercel, etc.)
// Set them from control panel, NOT in code:
// Heroku: Settings → Config Vars
// Vercel: Settings → Environment Variables
// Netlify: Site Settings → Environment Variables
Golden rule: If it’s secret, it goes in .env. If .env isn’t in .gitignore, you have a problem!
// 1. XSS Test
// Try entering this in every field:
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
// If you see the alert, you're vulnerable!
// 2. SAFE SQL Injection Tests (NON-destructive)
// In login/search field try:
' OR '1'='1
// This makes the condition always true
// If it logs you in or shows all results = VULNERABLE!
// Example of what happens:
// Original query: SELECT * FROM users WHERE email = '[input]'
// Becomes: SELECT * FROM users WHERE email = '' OR '1'='1'
// Result: Shows ALL users instead of one!
// Other safe tests:
admin' --
' OR 'a'='a
1' AND '1'='2
// 3. Validation test
// Try extreme values:
- Age: -1, 999, "hello"
- Email: "not-an-email"
- Phone: "SELECT * FROM users" (see if it's accepted)
- File: upload a .txt renamed as .jpg
// 4. Rate limiting test
// Try 10 wrong logins in a row (not 1000!)
// After the 5th it should block you
IMPORTANT: Only do these tests on:
You don’t need to implement everything immediately! Start with HTTPS, hashed passwords, and input validation. Then, depending on your ambitions for a real production launch, discuss with your AI which points to prioritize based on your specific project.
Before going live, check the relevant points for your app:
"add security to the app"
"make everything secure"
"implement security best practices"
"protect against all attacks"
"My app collects emails and passwords. Implement:
1. Password hashing with bcrypt (10 rounds)
2. Server-side email validation
3. Rate limiting: max 5 failed logins then block 15 min
4. Session token in httpOnly cookie
Show me the code and explain each part."
"I have a contact form. Protect it from XSS:
1. Sanitize name and message input
2. Escape output when displaying messages
3. Add CSRF token
4. Limit message length to 1000 characters
Use DOMPurify for sanitization."
Always remember:
If you see these signs, STOP and fix:
innerHTML with user dataPerfect security doesn’t exist, but well-done security does! You don’t need to build Fort Knox, but don’t leave the door wide open either.
Start with:
Then add the rest as you grow.
Remember: Better a simple, secure app than a complex, breached one!
“Security is like hygiene: you don’t notice when it’s there, but when it’s missing it’s a disaster!”
Now go forth, and may your users sleep soundly!