Security Best Practices
Protect your application from common security vulnerabilities
1. SQL Injection Prevention
Stabilize ORM automatically uses parameterized queries to prevent SQL injection:
// ✅ Safe: Parameterized query (automatic)const user = await repo.query().where("email", "=", userInput).findOne();// ❌ Dangerous: Raw SQL with user inputconst result = await orm.rawQuery(`SELECT * FROM users WHERE email = '${userInput}'`);// ✅ Safe: Raw query with parametersconst result = await orm.rawQuery("SELECT * FROM users WHERE email = ?",[userInput]);
2. Secure Database Credentials
Never hardcode credentials. Use environment variables:
.envtypescript
NEON_DATABASE_URL=postgresql://user:password@localhost:5432/myappREDIS_URL=redis://localhost:6379
db.config.tstypescript
import { DBType, type DBConfig } from "stabilize-orm";export const dbConfig: DBConfig = {type: DBType.Postgres,connectionString: process.env.DATABASE_URL, // From environment// Never commit credentials to version control};
Add .env to your .gitignore:
.gitignoretypescript
.env.env.local.env.*.local
3. Input Validation
Always validate and sanitize user input before database operations:
import { z } from "zod";// Define validation schemaconst userSchema = z.object({email: z.string().email().max(255),name: z.string().min(1).max(100),age: z.number().int().min(0).max(150),});// Validate before creatingasync function createUser(input: unknown) {try {const validated = userSchema.parse(input);return await repo.create(validated);} catch (error) {if (error instanceof z.ZodError) {throw new Error("Invalid input: " + error.message);}throw error;}}
4. Implement Row-Level Security
Ensure users can only access their own data:
// ❌ Bad: No authorization checkasync function getPost(postId: number) {return await postRepo.findById(postId);}// ✅ Good: Verify ownershipasync function getPost(postId: number, userId: number) {const post = await postRepo.query().where("id", "=", postId).where("userId", "=", userId) // Ensure user owns the post.findOne();if (!post) {throw new Error("Post not found or access denied");}return post;}// ✅ Better: Use query scopesexport const Post = defineModel({tableName: "posts",columns: { /* ... */ },scopes: {ownedBy: (query, userId: number) =>query.where("userId", "=", userId),},});// Usageconst userPosts = await postRepo.scope("ownedBy", currentUserId).find();
5. Protect Sensitive Data
Hash passwords and encrypt sensitive information:
import { hash, verify } from "@node-rs/argon2";// Hash password before storingasync function createUser(email: string, password: string) {const hashedPassword = await hash(password, {memoryCost: 19456,timeCost: 2,outputLen: 32,parallelism: 1,});return await userRepo.create({email,password: hashedPassword, // Store hashed password});}// Verify password on loginasync function login(email: string, password: string) {const user = await userRepo.query().where("email", "=", email).findOne();if (!user) {throw new Error("Invalid credentials");}const isValid = await verify(user.password, password);if (!isValid) {throw new Error("Invalid credentials");}return user;}
6. Use Transactions for Critical Operations
Ensure data consistency with transactions:
// ✅ Good: Use transaction for related operationsasync function transferFunds(fromId: number, toId: number, amount: number) {await orm.transaction(async (trx) => {// Deduct from senderconst sender = await accountRepo.findById(fromId, trx);if (sender.balance < amount) {throw new Error("Insufficient funds");}await accountRepo.update(fromId, {balance: sender.balance - amount,}, trx);// Add to receiverconst receiver = await accountRepo.findById(toId, trx);await accountRepo.update(toId, {balance: receiver.balance + amount,}, trx);// If any operation fails, entire transaction rolls back});}
7. Implement Rate Limiting
Protect against brute force attacks:
import { Ratelimit } from "@upstash/ratelimit";import { Redis } from "@upstash/redis";const redis = new Redis({url: process.env.REDIS_URL!,token: process.env.REDIS_TOKEN!,});const ratelimit = new Ratelimit({redis,limiter: Ratelimit.slidingWindow(5, "1 m"), // 5 requests per minute});async function login(email: string, password: string, ip: string) {// Check rate limitconst { success } = await ratelimit.limit(ip);if (!success) {throw new Error("Too many login attempts. Please try again later.");}// Proceed with login// ...}
8. Audit Logging
Track sensitive operations with lifecycle hooks:
export const User = defineModel({tableName: "users",columns: { /* ... */ },hooks: {afterCreate: async (user) => {await auditRepo.create({action: "USER_CREATED",userId: user.id,timestamp: new Date(),metadata: { email: user.email },});},afterUpdate: async (user, changes) => {await auditRepo.create({action: "USER_UPDATED",userId: user.id,timestamp: new Date(),metadata: { changes },});},afterDelete: async (user) => {await auditRepo.create({action: "USER_DELETED",userId: user.id,timestamp: new Date(),});},},});
9. Principle of Least Privilege
Grant database users only the permissions they need:
-- Create read-only user for reportingCREATE USER reporting_user WITH PASSWORD 'secure_password';GRANT SELECT ON ALL TABLES IN SCHEMA public TO reporting_user;-- Create app user with limited permissionsCREATE USER app_user WITH PASSWORD 'secure_password';GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;-- Don't grant DROP, TRUNCATE, or ALTER permissions to app users
Security Checklist
- ✓ Use parameterized queries (automatic in Stabilize)
- ✓ Store credentials in environment variables
- ✓ Validate all user input with schemas
- ✓ Implement row-level security checks
- ✓ Hash passwords with strong algorithms
- ✓ Use transactions for critical operations
- ✓ Implement rate limiting on sensitive endpoints
- ✓ Enable audit logging for compliance
- ✓ Follow principle of least privilege
- ✓ Keep dependencies updated
- ✓ Regular security audits
