feat: harden auth, security headers, SVG sanitization, and pipeline ownership

- Add password strength validation (8+ chars, uppercase, lowercase, number)
- Add username validation rules
- Optimize API key lookup with SHA-256 prefix (O(1) vs O(n) scan)
- Require password change on default admin first login
- Revoke API keys on password change
- Add session cleanup cron (hourly expired session purge)
- Add Permissions-Policy, HSTS, and CSP security headers in production
- Strengthen SVG sanitizer: block XInclude, foreignObject, processing
  instructions, javascript/data/file URI schemes
- Add userId ownership to pipelines with authorization checks
- Add keyPrefix column to api_keys table
- Update integration tests for new auth behavior
This commit is contained in:
Siddharth Kumar Sah
2026-03-24 21:38:06 +08:00
parent 75f38a9fe2
commit 432cc92471
13 changed files with 570 additions and 52 deletions
+20 -2
View File
@@ -17,6 +17,7 @@ import { validateImageBuffer } from "../lib/file-validation.js";
import { createWorkspace } from "../lib/workspace.js";
import { sanitizeFilename } from "../lib/filename.js";
import { db, schema } from "../db/index.js";
import { requireAuth, getAuthUser } from "../plugins/auth.js";
/** Schema for a single pipeline step. */
const pipelineStepSchema = z.object({
@@ -197,6 +198,9 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
app.post(
"/api/v1/pipeline/save",
async (request: FastifyRequest, reply: FastifyReply) => {
const user = requireAuth(request, reply);
if (!user) return;
const body = request.body as unknown;
const result = savePipelineSchema.safeParse(body);
@@ -227,6 +231,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
db.insert(schema.pipelines)
.values({
id,
userId: user.id,
name,
description: description ?? null,
steps: JSON.stringify(steps),
@@ -250,8 +255,13 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
*/
app.get(
"/api/v1/pipeline/list",
async (_request: FastifyRequest, reply: FastifyReply) => {
const rows = db.select().from(schema.pipelines).all();
async (request: FastifyRequest, reply: FastifyReply) => {
const user = requireAuth(request, reply);
if (!user) return;
// Users see their own pipelines + legacy pipelines (no owner)
const rows = db.select().from(schema.pipelines).all()
.filter(row => !row.userId || row.userId === user.id);
const pipelines = rows.map((row) => ({
id: row.id,
@@ -276,6 +286,9 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply,
) => {
const user = requireAuth(request, reply);
if (!user) return;
const { id } = request.params;
const existing = db
@@ -288,6 +301,11 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
return reply.status(404).send({ error: "Pipeline not found" });
}
// Only the owner (or admin) can delete; legacy pipelines (no owner) can be deleted by anyone
if (existing.userId && existing.userId !== user.id && user.role !== "admin") {
return reply.status(403).send({ error: "Not authorized to delete this pipeline" });
}
db.delete(schema.pipelines)
.where(eq(schema.pipelines.id, id))
.run();