Secure Development for Side Projects
Apply OWASP Top 10, CSP headers, secure defaults, CI/CD hardening, dependency scanning, and container security to your personal projects.
OWASP Top 10 for Personal Projects
You know the OWASP Top 10 from work. But when it's 1 AM and you're building a side project, security shortcuts feel harmless. "It's just a hobby app. Nobody's going to attack it."
Except they will. Automated scanners don't distinguish between Fortune 500 apps and your weekend project. If it's on the internet, it gets probed.
The Side Project Attack Surface
Your side project probably has:
- A web frontend (React, Vue, plain HTML)
- An API backend (Express, Flask, Go)
- A database (Postgres, SQLite, MongoDB)
- Authentication (maybe roll-your-own ๐ฌ)
- Deployment (Vercel, Railway, VPS)
- A domain with DNS
Each of these is a target.
OWASP Top 10 (2021) โ Side Project Focus
A01: Broken Access Control
The #1 vulnerability. In side projects, this usually manifests as:
// INSECURE: No authorization check
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user); // Any authenticated user can read ANY user's data
});
// SECURE: Verify the requesting user has access
app.get('/api/users/:id', authenticate, async (req, res) => {
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const user = await db.users.findById(req.params.id);
res.json(user);
});
A02: Cryptographic Failures
# INSECURE: Storing passwords in plaintext or with MD5
user.password = md5(request.form['password']) # โ Trivially crackable
# SECURE: Use bcrypt with proper cost factor
from bcrypt import hashpw, gensalt
user.password_hash = hashpw(
request.form['password'].encode(),
gensalt(rounds=12) # Cost factor 12 = ~250ms per hash
)
A03: Injection
# INSECURE: String concatenation SQL
query = f"SELECT * FROM users WHERE name = '{name}'" # โ SQL injection
# SECURE: Parameterized queries
cursor.execute("SELECT * FROM users WHERE name = %s", (name,)) # โ
# For ORMs (SQLAlchemy, Prisma, etc.), injection is handled automatically
# UNLESS you use raw queries โ always parameterize raw queries
A07: Identification and Authentication Failures
// INSECURE: Rolling your own auth
const token = jwt.sign({ userId: user.id }, 'secret123'); // โ Weak secret, no expiry
// SECURE: Use established auth libraries
// Better yet, use a managed auth service:
// - Clerk (free tier: 10K MAU)
// - Supabase Auth (free tier: 50K MAU)
// - Auth0 (free tier: 7K MAU)
// - NextAuth.js (self-hosted, well-maintained)
The golden rule for side projects: Don't build your own authentication. Use Supabase Auth, Clerk, Auth0, or at minimum NextAuth.js. Authentication is too important and too easy to get wrong.
A09: Security Logging and Monitoring Failures
// At minimum, log:
// 1. Authentication events (login, logout, failed attempts)
// 2. Authorization failures (403s)
// 3. Input validation failures
// 4. Errors and exceptions
app.use((err, req, res, next) => {
console.error(JSON.stringify({
timestamp: new Date().toISOString(),
method: req.method,
path: req.path,
ip: req.ip,
userId: req.user?.id,
error: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
}));
res.status(500).json({ error: 'Internal server error' });
// NEVER send stack traces to the client in production
});
Even for side projects, basic logging is essential. When your app gets probed (and it will), logs are how you detect and respond.