Fix FRE-622 security findings: IDOR, auth, markdown injection, email validation
H-1: Add createdBy to alertRules, IDOR check on update/delete H-2: Add createdBy to scheduledReports, IDOR check on update H-3: Add createdBy to cohorts, IDOR check on addCohortMember M-1: Change submitNPSResponse to protectedProcedure M-2: Escape Slack Markdown special chars in alert rule names M-3: Change getAllLatestKPIs, getAlertRules, getAlerts, getNPSResponses to protectedProcedure L-2: Add email regex validation to recipients field Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
|
||||
import { users } from "./users";
|
||||
|
||||
export const alertRules = sqliteTable("alert_rules", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
@@ -10,6 +11,7 @@ export const alertRules = sqliteTable("alert_rules", {
|
||||
channelId: text("channel_id"),
|
||||
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
||||
cooldownMinutes: integer("cooldown_minutes").notNull().default(60),
|
||||
createdBy: integer("created_by").references(() => users.id),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(new Date()),
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const cohorts = sqliteTable("cohorts", {
|
||||
size: integer("size").notNull().default(0),
|
||||
retentionData: text("retention_data"),
|
||||
metadata: text("metadata"),
|
||||
createdBy: integer("created_by").references(() => users.id),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(new Date()),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||
import { users } from "./users";
|
||||
|
||||
export const scheduledReports = sqliteTable("scheduled_reports", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
@@ -11,6 +12,7 @@ export const scheduledReports = sqliteTable("scheduled_reports", {
|
||||
lastRunAt: integer("last_run_at", { mode: "timestamp" }),
|
||||
nextRunAt: integer("next_run_at", { mode: "timestamp" }),
|
||||
metadata: text("metadata"),
|
||||
createdBy: integer("created_by").references(() => users.id),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(new Date()),
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface CohortDefinition {
|
||||
periodStart: Date;
|
||||
periodEnd?: Date;
|
||||
filterCriteria: Record<string, unknown>;
|
||||
createdBy?: number;
|
||||
}
|
||||
|
||||
export interface CohortAnalysisResult {
|
||||
@@ -37,6 +38,7 @@ export async function createCohort(
|
||||
size: 0,
|
||||
retentionData: null,
|
||||
metadata: definition.description ? JSON.stringify({ description: definition.description }) : null,
|
||||
createdBy: definition.createdBy ?? null,
|
||||
};
|
||||
|
||||
const result = await db.insert(cohorts).values(cohort).returning();
|
||||
|
||||
@@ -108,6 +108,18 @@ function mapSeverity(
|
||||
return rule.severity;
|
||||
}
|
||||
|
||||
function escapeSlackMarkdown(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\*/g, '\\*')
|
||||
.replace(/_/g, '\\_')
|
||||
.replace(/~/g, '\\~')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\(/g, '\\(');
|
||||
}
|
||||
|
||||
export function formatAlertMessage(
|
||||
rule: typeof alertRules.$inferSelect,
|
||||
value: number,
|
||||
@@ -118,7 +130,9 @@ export function formatAlertMessage(
|
||||
? rule.threshold.toString()
|
||||
: rule.threshold.toFixed(2);
|
||||
|
||||
return `${rule.name}: ${kpiKey} is ${formattedValue} (${rule.condition} ${formattedThreshold})`;
|
||||
const safeName = escapeSlackMarkdown(rule.name);
|
||||
|
||||
return `${safeName}: ${kpiKey} is ${formattedValue} (${rule.condition} ${formattedThreshold})`;
|
||||
}
|
||||
|
||||
export async function sendSlackAlert(
|
||||
|
||||
Reference in New Issue
Block a user