First Init
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import { defineMiddleware } from 'astro:middleware';
|
||||
import { logger } from './utils/logger';
|
||||
import { captureException } from './utils/sentry';
|
||||
|
||||
/**
|
||||
* Security Middleware
|
||||
* Implements security headers and domain validation for workroot.in
|
||||
*
|
||||
* Security Headers Implemented:
|
||||
* - Content-Security-Policy (CSP): Controls resource loading
|
||||
* - X-Frame-Options: Prevents clickjacking
|
||||
* - X-Content-Type-Options: Prevents MIME sniffing
|
||||
* - Referrer-Policy: Controls referrer information
|
||||
* - Permissions-Policy: Controls browser features
|
||||
* - Strict-Transport-Security (HSTS): Enforces HTTPS
|
||||
*/
|
||||
|
||||
// CSRF protection: validate Origin/Referer for state-changing API requests
|
||||
function validateCsrfOrigin(request: Request, url: URL): boolean {
|
||||
const method = request.method;
|
||||
const path = url.pathname;
|
||||
|
||||
// Only enforce on state-changing API methods
|
||||
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) return true;
|
||||
if (!path.startsWith('/api/')) return true;
|
||||
|
||||
// In development, allow any origin
|
||||
if (!import.meta.env.PROD) return true;
|
||||
|
||||
const origin = request.headers.get('origin');
|
||||
const referer = request.headers.get('referer');
|
||||
|
||||
if (origin) {
|
||||
return origin === 'https://workroot.in' || origin === 'https://www.workroot.in';
|
||||
}
|
||||
if (referer) {
|
||||
return referer.startsWith('https://workroot.in') || referer.startsWith('https://www.workroot.in');
|
||||
}
|
||||
|
||||
// If neither origin nor referer is present, reject (missing headers = suspicious)
|
||||
return false;
|
||||
}
|
||||
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
const startTime = Date.now();
|
||||
const method = context.request.method;
|
||||
const path = context.url.pathname;
|
||||
|
||||
// CSRF origin validation for API mutations
|
||||
if (!validateCsrfOrigin(context.request, context.url)) {
|
||||
logger.warn('middleware', 'CSRF origin check failed', {
|
||||
method,
|
||||
path,
|
||||
origin: context.request.headers.get('origin'),
|
||||
referer: context.request.headers.get('referer'),
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, error: 'Forbidden: invalid origin.' }),
|
||||
{ status: 403, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await next();
|
||||
} catch (err) {
|
||||
const durationMs = Date.now() - startTime;
|
||||
await captureException(err, {
|
||||
scope: 'middleware',
|
||||
extra: { method, path, durationMs },
|
||||
});
|
||||
// Re-throw so Astro can render its 500 page
|
||||
throw err;
|
||||
}
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
const status = response.status;
|
||||
|
||||
// Log all SSR errors (5xx) and not-found (404) responses
|
||||
if (status >= 500) {
|
||||
logger.error('middleware', `SSR error ${status}`, { method, path, status, durationMs });
|
||||
} else if (status === 404) {
|
||||
logger.warn('middleware', `Not found`, { method, path, status, durationMs });
|
||||
} else if (status >= 400) {
|
||||
logger.warn('middleware', `Client error ${status}`, { method, path, status, durationMs });
|
||||
} else {
|
||||
logger.debug('middleware', `Request`, { method, path, status, durationMs });
|
||||
}
|
||||
|
||||
// Clone the response to add headers
|
||||
const modifiedResponse = new Response(response.body, response);
|
||||
|
||||
// Content Security Policy (CSP)
|
||||
// Allows:
|
||||
// - self: Same-origin resources
|
||||
// - https://images.unsplash.com: External images (explicit, not wildcard https:)
|
||||
// - https://fonts.googleapis.com & https://fonts.gstatic.com: Google Fonts
|
||||
// - inline scripts/styles: Required for Astro framework
|
||||
// - https://www.googletagmanager.com & https://www.google-analytics.com: GA4
|
||||
// - https://plausible.io: Plausible Analytics
|
||||
// Note: unsafe-eval is removed in production (only needed during Astro HMR dev mode)
|
||||
const isProduction = import.meta.env.PROD;
|
||||
const scriptSrc = isProduction
|
||||
? "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io"
|
||||
: "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io";
|
||||
|
||||
const csp = [
|
||||
"default-src 'self'",
|
||||
scriptSrc,
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||
"img-src 'self' data: https://images.unsplash.com https://www.google-analytics.com https://www.googletagmanager.com",
|
||||
"font-src 'self' data: https://fonts.gstatic.com",
|
||||
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://plausible.io",
|
||||
"worker-src 'self'",
|
||||
"manifest-src 'self'",
|
||||
"object-src 'none'", // Block Flash/plugins (defense-in-depth)
|
||||
"frame-src 'none'", // No embedded frames allowed
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
"upgrade-insecure-requests",
|
||||
].join('; ');
|
||||
|
||||
modifiedResponse.headers.set('Content-Security-Policy', csp);
|
||||
|
||||
// X-Frame-Options: Prevent clickjacking
|
||||
// DENY prevents any domain from framing the content
|
||||
modifiedResponse.headers.set('X-Frame-Options', 'DENY');
|
||||
|
||||
// X-Content-Type-Options: Prevent MIME sniffing
|
||||
// nosniff prevents browsers from interpreting files as different MIME type
|
||||
modifiedResponse.headers.set('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// Referrer-Policy: Control referrer information
|
||||
// strict-origin-when-cross-origin sends full URL for same-origin, origin only for cross-origin
|
||||
modifiedResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// Permissions-Policy: Disable unnecessary browser features
|
||||
// Restricts access to sensitive APIs to prevent abuse
|
||||
modifiedResponse.headers.set('Permissions-Policy',
|
||||
'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()'
|
||||
);
|
||||
|
||||
// Strict-Transport-Security (HSTS): Enforce HTTPS
|
||||
// max-age=31536000: 1 year
|
||||
// includeSubDomains: Apply to all subdomains
|
||||
// preload: Allow inclusion in HSTS preload list
|
||||
// Only set HSTS in production and when using HTTPS
|
||||
const isHttps = context.url.protocol === 'https:';
|
||||
|
||||
if (isProduction && isHttps) {
|
||||
modifiedResponse.headers.set(
|
||||
'Strict-Transport-Security',
|
||||
'max-age=31536000; includeSubDomains; preload'
|
||||
);
|
||||
}
|
||||
|
||||
// X-Permitted-Cross-Domain-Policies: Restrict cross-domain access
|
||||
modifiedResponse.headers.set('X-Permitted-Cross-Domain-Policies', 'none');
|
||||
|
||||
// X-DNS-Prefetch-Control: Control DNS prefetching
|
||||
modifiedResponse.headers.set('X-DNS-Prefetch-Control', 'on');
|
||||
|
||||
// Domain validation: Ensure requests are for workroot.in
|
||||
const allowedHosts = ['workroot.in', 'www.workroot.in', 'localhost', '127.0.0.1', '0.0.0.0'];
|
||||
const requestHost = context.url.hostname;
|
||||
|
||||
// In development, allow any host
|
||||
if (!isProduction || allowedHosts.includes(requestHost)) {
|
||||
return modifiedResponse;
|
||||
}
|
||||
|
||||
// In production, if host doesn't match, redirect to canonical domain
|
||||
if (isProduction && !allowedHosts.includes(requestHost)) {
|
||||
const canonicalUrl = `https://workroot.in${context.url.pathname}${context.url.search}`;
|
||||
return Response.redirect(canonicalUrl, 301);
|
||||
}
|
||||
|
||||
return modifiedResponse;
|
||||
});
|
||||
Reference in New Issue
Block a user