Implement OWASP ASVS L1 security remediation (Phases 0-2)
Auth: Add requireAuth/requireAdmin guards with JWT cookie verification, member status checks (suspended/cancelled = 403), and admin role enforcement. Apply to all admin, upload, and payment endpoints. Add role field to Member model. CSRF: Double-submit cookie middleware with client plugin. Exempt webhook and magic-link verify routes. Headers: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy on all responses. HSTS and CSP (Helcim/Cloudinary/Plausible sources) in production only. Rate limiting: Auth 5/5min, payment 10/min, upload 10/min, general 100/min via rate-limiter-flexible, keyed by client IP. XSS: DOMPurify sanitization on marked() output with tag/attr allowlists. escapeHtml() utility for email template interpolation. Anti-enumeration: Login returns identical response for existing and non-existing emails. Remove 404 handling from login UI components. Mass assignment: Remove helcimCustomerId from profile allowedFields. Session: 7-day token expiry, refresh endpoint, httpOnly+secure cookies. Environment: Validate required secrets on startup via server plugin. Remove JWT_SECRET hardcoded fallback.
This commit is contained in:
parent
29c96a207e
commit
26c300c357
41 changed files with 566 additions and 380 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { Resend } from "resend";
|
||||
import { escapeHtml } from "./escapeHtml.js";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ export async function sendEventRegistrationEmail(registration, eventData) {
|
|||
const { data, error } = await resend.emails.send({
|
||||
from: "Ghost Guild <events@babyghosts.org>",
|
||||
to: [registration.email],
|
||||
subject: `You're registered for ${eventData.title}`,
|
||||
subject: `You're registered for ${escapeHtml(eventData.title)}`,
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
|
@ -105,9 +106,9 @@ export async function sendEventRegistrationEmail(registration, eventData) {
|
|||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Hi ${registration.name},</p>
|
||||
<p>Hi ${escapeHtml(registration.name)},</p>
|
||||
|
||||
<p>Thank you for registering for <strong>${eventData.title}</strong>!</p>
|
||||
<p>Thank you for registering for <strong>${escapeHtml(eventData.title)}</strong>!</p>
|
||||
|
||||
<div class="event-details">
|
||||
<div class="detail-row">
|
||||
|
|
@ -122,11 +123,11 @@ export async function sendEventRegistrationEmail(registration, eventData) {
|
|||
|
||||
<div class="detail-row">
|
||||
<div class="label">Location</div>
|
||||
<div class="value">${eventData.location}</div>
|
||||
<div class="value">${escapeHtml(eventData.location)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${eventData.description ? `<p>${eventData.description}</p>` : ""}
|
||||
${eventData.description ? `<p>${escapeHtml(eventData.description)}</p>` : ""}
|
||||
|
||||
${
|
||||
registration.ticketType &&
|
||||
|
|
@ -148,7 +149,7 @@ export async function sendEventRegistrationEmail(registration, eventData) {
|
|||
? `
|
||||
<div class="detail-row">
|
||||
<div class="label">Transaction ID</div>
|
||||
<div class="value" style="font-size: 12px; font-family: monospace;">${registration.paymentId}</div>
|
||||
<div class="value" style="font-size: 12px; font-family: monospace;">${escapeHtml(registration.paymentId)}</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
|
|
@ -211,7 +212,7 @@ export async function sendEventCancellationEmail(registration, eventData) {
|
|||
const { data, error } = await resend.emails.send({
|
||||
from: "Ghost Guild <events@ghostguild.org>",
|
||||
to: [registration.email],
|
||||
subject: `Registration cancelled: ${eventData.title}`,
|
||||
subject: `Registration cancelled: ${escapeHtml(eventData.title)}`,
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
|
@ -264,9 +265,9 @@ export async function sendEventCancellationEmail(registration, eventData) {
|
|||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Hi ${registration.name},</p>
|
||||
<p>Hi ${escapeHtml(registration.name)},</p>
|
||||
|
||||
<p>Your registration for <strong>${eventData.title}</strong> has been cancelled.</p>
|
||||
<p>Your registration for <strong>${escapeHtml(eventData.title)}</strong> has been cancelled.</p>
|
||||
|
||||
<p>We're sorry you can't make it. You can always register again if your plans change.</p>
|
||||
|
||||
|
|
@ -332,7 +333,7 @@ export async function sendWaitlistNotificationEmail(waitlistEntry, eventData) {
|
|||
const { data, error } = await resend.emails.send({
|
||||
from: "Ghost Guild <events@ghostguild.org>",
|
||||
to: [waitlistEntry.email],
|
||||
subject: `A spot opened up for ${eventData.title}!`,
|
||||
subject: `A spot opened up for ${escapeHtml(eventData.title)}!`,
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
|
@ -413,9 +414,9 @@ export async function sendWaitlistNotificationEmail(waitlistEntry, eventData) {
|
|||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Hi ${waitlistEntry.name},</p>
|
||||
<p>Hi ${escapeHtml(waitlistEntry.name)},</p>
|
||||
|
||||
<p>Great news! A spot has become available for <strong>${eventData.title}</strong>, and you're on the waitlist.</p>
|
||||
<p>Great news! A spot has become available for <strong>${escapeHtml(eventData.title)}</strong>, and you're on the waitlist.</p>
|
||||
|
||||
<div class="urgent">
|
||||
<p style="margin: 0; font-weight: 600; color: #92400e;">
|
||||
|
|
@ -426,7 +427,7 @@ export async function sendWaitlistNotificationEmail(waitlistEntry, eventData) {
|
|||
<div class="event-details">
|
||||
<div class="detail-row">
|
||||
<div class="label">Event</div>
|
||||
<div class="value">${eventData.title}</div>
|
||||
<div class="value">${escapeHtml(eventData.title)}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
|
|
@ -441,7 +442,7 @@ export async function sendWaitlistNotificationEmail(waitlistEntry, eventData) {
|
|||
|
||||
<div class="detail-row">
|
||||
<div class="label">Location</div>
|
||||
<div class="value">${eventData.location}</div>
|
||||
<div class="value">${escapeHtml(eventData.location)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -529,7 +530,7 @@ export async function sendSeriesPassConfirmation(options) {
|
|||
const { data, error } = await resend.emails.send({
|
||||
from: "Ghost Guild <events@babyghosts.org>",
|
||||
to: [to],
|
||||
subject: `Your Series Pass for ${series.title}`,
|
||||
subject: `Your Series Pass for ${escapeHtml(series.title)}`,
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
|
@ -620,10 +621,10 @@ export async function sendSeriesPassConfirmation(options) {
|
|||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p style="font-size: 18px; margin-bottom: 10px;">Hi ${name},</p>
|
||||
<p style="font-size: 18px; margin-bottom: 10px;">Hi ${escapeHtml(name)},</p>
|
||||
|
||||
<p>
|
||||
Great news! Your series pass for <strong>${series.title}</strong> is confirmed.
|
||||
Great news! Your series pass for <strong>${escapeHtml(series.title)}</strong> is confirmed.
|
||||
You're now registered for all ${events.length} events in this ${seriesTypeLabels[series.type] || "series"}.
|
||||
</p>
|
||||
|
||||
|
|
@ -647,7 +648,7 @@ export async function sendSeriesPassConfirmation(options) {
|
|||
|
||||
<div class="detail-row">
|
||||
<div class="label">Series</div>
|
||||
<div class="value">${series.title}</div>
|
||||
<div class="value">${escapeHtml(series.title)}</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
|
|
@ -655,7 +656,7 @@ export async function sendSeriesPassConfirmation(options) {
|
|||
? `
|
||||
<div class="detail-row">
|
||||
<div class="label">About</div>
|
||||
<div class="value">${series.description}</div>
|
||||
<div class="value">${escapeHtml(series.description)}</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
|
|
@ -676,7 +677,7 @@ export async function sendSeriesPassConfirmation(options) {
|
|||
? `
|
||||
<div class="detail-row">
|
||||
<div class="label">Transaction ID</div>
|
||||
<div class="value" style="font-family: monospace; font-size: 14px;">${paymentId}</div>
|
||||
<div class="value" style="font-family: monospace; font-size: 14px;">${escapeHtml(paymentId)}</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
|
|
@ -699,7 +700,7 @@ export async function sendSeriesPassConfirmation(options) {
|
|||
(event, index) => `
|
||||
<div class="event-item">
|
||||
<div style="font-weight: 600; color: #7c3aed; margin-bottom: 5px;">
|
||||
Event ${index + 1}: ${event.title}
|
||||
Event ${index + 1}: ${escapeHtml(event.title)}
|
||||
</div>
|
||||
<div style="font-size: 14px; color: #666; margin: 5px 0;">
|
||||
📅 ${formatDate(event.startDate)}
|
||||
|
|
@ -708,7 +709,7 @@ export async function sendSeriesPassConfirmation(options) {
|
|||
🕐 ${formatTime(event.startDate, event.endDate)}
|
||||
</div>
|
||||
<div style="font-size: 14px; color: #666; margin: 5px 0;">
|
||||
📍 ${event.location}
|
||||
📍 ${escapeHtml(event.location)}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue