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 input
const result = await orm.rawQuery(
`SELECT * FROM users WHERE email = '${userInput}'`
);
// ✅ Safe: Raw query with parameters
const 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/myapp
REDIS_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 schema
const 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 creating
async 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 check
async function getPost(postId: number) {
return await postRepo.findById(postId);
}
// ✅ Good: Verify ownership
async 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 scopes
export const Post = defineModel({
tableName: "posts",
columns: { /* ... */ },
scopes: {
ownedBy: (query, userId: number) =>
query.where("userId", "=", userId),
},
});
// Usage
const 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 storing
async 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 login
async 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 operations
async function transferFunds(fromId: number, toId: number, amount: number) {
await orm.transaction(async (trx) => {
// Deduct from sender
const 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 receiver
const 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 limit
const { 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 reporting
CREATE USER reporting_user WITH PASSWORD 'secure_password';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO reporting_user;
-- Create app user with limited permissions
CREATE 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