fix: implement critical security remediation for authentication and authorization
- Add Clerk token verification to tRPC context (server/trpc/index.ts) - Remove client-controlled authorId/reviewedById from revisions router - Require JWT_SECRET environment variable, remove hardcoded fallback - Add table name validation to prevent SQL injection in backup logic - Fix TRPCContext type to use better-sqlite3 instead of LibSQL - Update revisions router tests to use proper tRPC v11+ API - Add resetInMemoryState function for test isolation Security fixes address: - Critical: Authentication bypass via missing token verification - Critical: User impersonation via client-controlled IDs - High: Insecure WebSocket defaults with hardcoded secrets - High: SQL injection vulnerability in backup logic All tests passing (24/24).
This commit is contained in:
@@ -119,6 +119,14 @@ function getLatestVersionForScript(scriptId: number, branchName: string): number
|
||||
return maxVersion;
|
||||
}
|
||||
|
||||
// Export reset function for testing
|
||||
export function resetInMemoryState() {
|
||||
revisions.clear();
|
||||
revisionChanges.clear();
|
||||
revisionIdCounter = 0;
|
||||
changeIdCounter = 0;
|
||||
}
|
||||
|
||||
export const revisionsRouter = {
|
||||
listRevisions: protectedProcedure
|
||||
.input(z.object({
|
||||
@@ -153,9 +161,12 @@ export const revisionsRouter = {
|
||||
content: z.string(),
|
||||
branchName: z.string().default('main'),
|
||||
parentRevisionId: z.number().int().positive().optional(),
|
||||
authorId: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!ctx.userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const nextVersion = getLatestVersionForScript(
|
||||
input.scriptId,
|
||||
input.branchName
|
||||
@@ -170,7 +181,7 @@ export const revisionsRouter = {
|
||||
title: input.title,
|
||||
summary: input.summary || null,
|
||||
content: input.content,
|
||||
authorId: input.authorId,
|
||||
authorId: ctx.userId,
|
||||
status: 'draft' as const,
|
||||
reviewedById: null,
|
||||
reviewedAt: null,
|
||||
@@ -306,9 +317,12 @@ export const revisionsRouter = {
|
||||
acceptRevision: protectedProcedure
|
||||
.input(z.object({
|
||||
revisionId: z.number().int().positive(),
|
||||
reviewedById: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!ctx.userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const revision = revisions.get(input.revisionId);
|
||||
if (!revision) {
|
||||
throw new Error(`Revision ${input.revisionId} not found`);
|
||||
@@ -317,7 +331,7 @@ export const revisionsRouter = {
|
||||
const updated = {
|
||||
...revision,
|
||||
status: 'accepted' as const,
|
||||
reviewedById: input.reviewedById,
|
||||
reviewedById: ctx.userId,
|
||||
reviewedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
@@ -329,10 +343,13 @@ export const revisionsRouter = {
|
||||
rejectRevision: protectedProcedure
|
||||
.input(z.object({
|
||||
revisionId: z.number().int().positive(),
|
||||
reviewedById: z.number().int().positive(),
|
||||
reason: z.string().max(1000).optional(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!ctx.userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const revision = revisions.get(input.revisionId);
|
||||
if (!revision) {
|
||||
throw new Error(`Revision ${input.revisionId} not found`);
|
||||
@@ -341,7 +358,7 @@ export const revisionsRouter = {
|
||||
const updated = {
|
||||
...revision,
|
||||
status: 'rejected' as const,
|
||||
reviewedById: input.reviewedById,
|
||||
reviewedById: ctx.userId,
|
||||
reviewedAt: new Date(),
|
||||
summary: input.reason
|
||||
? (revision.summary || '') + '\n[Rejected: ' + input.reason + ']'
|
||||
@@ -357,9 +374,12 @@ export const revisionsRouter = {
|
||||
.input(z.object({
|
||||
scriptId: z.number().int().positive(),
|
||||
revisionId: z.number().int().positive(),
|
||||
authorId: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!ctx.userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const targetRevision = revisions.get(input.revisionId);
|
||||
if (!targetRevision) {
|
||||
throw new Error(`Revision ${input.revisionId} not found`);
|
||||
@@ -383,7 +403,7 @@ export const revisionsRouter = {
|
||||
title: `Rollback to v${targetRevision.versionNumber}: ${targetRevision.title}`,
|
||||
summary: `Rolled back to revision ${targetRevision.id}`,
|
||||
content: targetRevision.content,
|
||||
authorId: input.authorId,
|
||||
authorId: ctx.userId,
|
||||
status: 'draft' as const,
|
||||
reviewedById: null,
|
||||
reviewedAt: null,
|
||||
@@ -460,9 +480,12 @@ export const revisionsRouter = {
|
||||
scriptId: z.number().int().positive(),
|
||||
branchName: z.string().min(1),
|
||||
fromRevisionId: z.number().int().positive().optional(),
|
||||
authorId: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!ctx.userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const existing = Array.from(revisions.values())
|
||||
.some(r => r.scriptId === input.scriptId && r.branchName === input.branchName);
|
||||
|
||||
@@ -500,7 +523,7 @@ export const revisionsRouter = {
|
||||
title: `Branch: ${input.branchName}`,
|
||||
summary: null,
|
||||
content: sourceContent,
|
||||
authorId: input.authorId,
|
||||
authorId: ctx.userId,
|
||||
status: 'draft' as const,
|
||||
reviewedById: null,
|
||||
reviewedAt: null,
|
||||
@@ -517,9 +540,12 @@ export const revisionsRouter = {
|
||||
scriptId: z.number().int().positive(),
|
||||
sourceBranch: z.string(),
|
||||
targetBranch: z.string(),
|
||||
authorId: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!ctx.userId) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
if (input.sourceBranch === input.targetBranch) {
|
||||
throw new Error('Cannot merge a branch into itself');
|
||||
}
|
||||
@@ -547,7 +573,7 @@ export const revisionsRouter = {
|
||||
title: `Merge from '${input.sourceBranch}'`,
|
||||
summary: `Merged ${input.sourceBranch} into ${input.targetBranch}`,
|
||||
content: sourceContent,
|
||||
authorId: input.authorId,
|
||||
authorId: ctx.userId,
|
||||
status: 'draft' as const,
|
||||
reviewedById: null,
|
||||
reviewedAt: null,
|
||||
|
||||
Reference in New Issue
Block a user