Newer
Older
express-blog / src / routes / contact.js
// // src/routes/contact.js
// const express = require("express");
// const router = express.Router();
// const sendContactMail = require("../utils/sendContactMail");
// const getBaseContext = require("../utils/baseContext");
// const formLimiter = require("../utils/formLimiter");
// const verifyHCaptcha = require("../utils/verifyHCaptcha");

// router.post("/contact", formLimiter, async (req, res, next) => {
//   try {
//     const { name, email, message, hcaptchaToken } = req.body;
//     if (!hcaptchaToken) {
//       return res.status(400).send("Captcha token missing");
//     }
//     const valid = await verifyHCaptcha(hcaptchaToken);
//     if (!valid) {
//       return res.status(400).send("Captcha verification failed");
//     }
//     await sendContactMail({ name, email, message });
//     res.redirect("/contact/thankyou");
//   } catch (err) {
//     next(err);
//   }
// });

// router.get("/contact", async (req, res) => {
//   const context = await getBaseContext({
//     csrfToken: res.locals.csrfToken,
//     title: "Contact",
//   });
//   res.render("pages/contact.handlebars", context);
// });

// router.get("/contact/thankyou", async (req, res) => {
//   const context = await getBaseContext({
//     title: "Thank You",
//   });
//   res.render("pages/thankyou.handlebars", context);
// });

// module.exports = router;
// src/routes/contact.js
const express = require("express");
const router = express.Router();
const sendContactMail = require("../utils/sendContactMail");
// const getBaseContext = require("../utils/baseContext");
const formLimiter = require("../utils/formLimiter");
const verifyHCaptcha = require("../utils/verifyHCaptcha");
const crypto = require("crypto");
const fs = require("fs").promises;
const path = require("path");
const HttpError = require("../utils/HttpError");
const { baseUrl } = require("../utils/baseUrl");
const { qualifyLink } = require("../utils/qualifyLinks");

const { winstonLogger } = require("../utils/logging");

// Threat detection patterns
const THREAT_PATTERNS = {
  // Common phishing/spam indicators
  suspiciousKeywords: [
    "verify account",
    "urgent action",
    "suspended account",
    "click here",
    "limited time",
    "act now",
    "confirm identity",
    "update payment",
    "security alert",
    "unusual activity",
  ],

  // Suspicious domains (add known bad actor domains)
  suspiciousDomains: [
    "tempmail.org",
    "10minutemail.com",
    "guerrillamail.com",
    "throwaway.email",
    "temp-mail.org",
  ],

  // Suspicious patterns
  suspiciousPatterns: [
    /https?:\/\/[^\s]+/gi, // URLs in messages
    /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, // Email addresses
    /\b(?:\d{4}[-\s]?){3}\d{4}\b/g, // Credit card patterns
    /\b\d{3}-\d{2}-\d{4}\b/g, // SSN patterns
  ],
};

// Enhanced forensic data collection (focused on threat detection)
function captureSecurityData(req, additionalData = {}) {
  const timestamp = new Date().toISOString();
  const requestId = crypto.randomUUID();

  // Connection and network data
  const connectionData = {
    ip: req.ip,
    ips: req.ips || [],
    remoteAddress: req.socket?.remoteAddress,
    protocol: req.protocol,
    secure: req.secure,
    hostname: req.hostname,
    originalUrl: req.originalUrl,
    encrypted: req.socket?.encrypted || false,
  };

  // Security-relevant headers
  const securityHeaders = {
    userAgent: req.headers["user-agent"],
    acceptLanguage: req.headers["accept-language"],
    referer: req.headers["referer"],
    origin: req.headers["origin"],
    xForwardedFor: req.headers["x-forwarded-for"],
    xRealIp: req.headers["x-real-ip"],
    host: req.headers["host"],
    // Check for proxy/VPN indicators
    via: req.headers["via"],
    xForwardedProto: req.headers["x-forwarded-proto"],
    cfConnectingIp: req.headers["cf-connecting-ip"], // Cloudflare
    cfIpCountry: req.headers["cf-ipcountry"],
    cfRay: req.headers["cf-ray"],
  };

  // Request timing and patterns
  const requestData = {
    method: req.method,
    url: req.url,
    path: req.path,
    query: req.query,
    timestamp: timestamp,
    requestStart: req._startTime || Date.now(),
    processingTime: Date.now() - (req._startTime || Date.now()),
  };

  // TLS/Security info
  let tlsData = null;
  if (req.socket && req.socket.encrypted) {
    try {
      const cipher = req.socket.getCipher ? req.socket.getCipher() : null;
      tlsData = {
        cipher: cipher,
        tlsVersion: req.socket.getProtocol ? req.socket.getProtocol() : null,
        authorized: req.socket.authorized,
      };
    } catch (err) {
      tlsData = { error: "TLS data unavailable" };
    }
  }

  return {
    requestId,
    timestamp,
    connection: connectionData,
    security: securityHeaders,
    request: requestData,
    tls: tlsData,
    additional: additionalData,
  };
}

// Threat analysis function
function analyzeThreatLevel(formData, securityData) {
  let threatScore = 0;
  const indicators = [];

  // Check message content for suspicious patterns
  const message = formData.message?.toLowerCase() || "";
  const email = formData.email?.toLowerCase() || "";
  const name = formData.name?.toLowerCase() || "";

  // Suspicious keywords in message
  THREAT_PATTERNS.suspiciousKeywords.forEach((keyword) => {
    if (message.includes(keyword.toLowerCase())) {
      threatScore += 3;
      indicators.push(`suspicious_keyword: ${keyword}`);
    }
  });

  // Check for suspicious email domains
  const emailDomain = email.split("@")[1];
  if (emailDomain && THREAT_PATTERNS.suspiciousDomains.includes(emailDomain)) {
    threatScore += 5;
    indicators.push(`suspicious_email_domain: ${emailDomain}`);
  }

  // Check for suspicious patterns in content
  THREAT_PATTERNS.suspiciousPatterns.forEach((pattern, index) => {
    if (pattern.test(message)) {
      threatScore += 2;
      indicators.push(`suspicious_pattern_${index}`);
    }
  });

  // Check for rapid form submission (potential automation)
  if (securityData.additional.clientData?.formTime < 5000) {
    // Less than 5 seconds
    threatScore += 2;
    indicators.push("rapid_submission");
  }

  // Check for suspicious user agent
  const userAgent = securityData.security.userAgent || "";
  if (!userAgent || userAgent.includes("bot") || userAgent.includes("crawl")) {
    threatScore += 3;
    indicators.push("suspicious_user_agent");
  }

  // Check for missing referer (direct access)
  if (!securityData.security.referer) {
    threatScore += 1;
    indicators.push("no_referer");
  }

  // Determine threat level
  let threatLevel = "low";
  if (threatScore >= 8) threatLevel = "high";
  else if (threatScore >= 4) threatLevel = "medium";

  return {
    score: threatScore,
    level: threatLevel,
    indicators: indicators,
    requiresReview: threatScore >= 4,
  };
}

async function logSecurityEvent(data, eventType = "contact_submission") {
  try {
    const date = new Date().toISOString().split("T")[0];
    const logEntry = {
      eventType,
      ...data,
      loggedAt: new Date().toISOString(),
    };

    // Log security event at custom 'security' level
    winstonLogger.log("security", logEntry);

    // Separate high-threat log file
    if (data.threatAnalysis?.level === "high") {
      const logDir = path.join(__dirname, "..", "..", "logs", "security");
      await fs.mkdir(logDir, { recursive: true });

      const alertFile = path.join(logDir, `high_threat_${date}.log`);
      await fs.appendFile(alertFile, message + "\n");
    }
  } catch (err) {
    // Fail silently or log to error log, depending on requirements
    winstonLogger.error(`Failed to log security event: ${err.message}`);
  }
}

module.exports = { logSecurityEvent };

// Middleware to capture request start time
router.use((req, res, next) => {
  req._startTime = Date.now();
  next();
});

router.post("/contact", formLimiter, async (req, res, next) => {
  try {
    const { name, email, message, subject, hcaptchaToken, clientData } =
      req.body;
    // Basic input validation
    function isValidEmail(email) {
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && email.length <= 254;
    }

    function isReasonableLength(str, maxLen) {
      return (
        typeof str === "string" && str.trim().length > 0 && str.length <= maxLen
      );
    }

    if (
      !isReasonableLength(name, 100) ||
      !isValidEmail(email) ||
      !isReasonableLength(subject, 150) ||
      !isReasonableLength(message, 2000)
    ) {
      const invalidData = captureSecurityData(req, {
        formData: { name, email, subject, message },
        failureReason: "invalid_input",
        processingStep: "validation",
      });

      await logSecurityEvent(invalidData, "validation_failure");
      return next(new HttpError("Invalid input", 400));
    }
    // Capture security data
    const securityData = captureSecurityData(req, {
      formData: { name, email, message, subject },
      captchaProvided: !!hcaptchaToken,
      clientData: clientData, // From client-side
      processingStep: "initial_validation",
    });

    // Analyze threat level
    const threatAnalysis = analyzeThreatLevel(
      { name, email, message, subject },
      securityData
    );

    // Enhanced logging with threat analysis
    await logSecurityEvent(
      {
        ...securityData,
        threatAnalysis: threatAnalysis,
        formData: { name, email, hasMessage: !!message, hasSubject: !!subject },
      },
      "contact_submission"
    );

    // CAPTCHA validation
    if (!hcaptchaToken) {
      await logSecurityEvent(
        {
          ...securityData,
          threatAnalysis: threatAnalysis,
          validationResult: "failed",
          failureReason: "missing_captcha",
        },
        "validation_failure"
      );

      return next(new HttpError("Captcha token missing", 400));
    }

    const valid = await verifyHCaptcha(hcaptchaToken);
    if (!valid) {
      await logSecurityEvent(
        {
          ...securityData,
          threatAnalysis: threatAnalysis,
          validationResult: "failed",
          failureReason: "captcha_failed",
        },
        "validation_failure"
      );

      return next(new HttpError("Captcha verification failed", 400));
    }

    // High threat handling
    if (threatAnalysis.level === "high") {
      await logSecurityEvent(
        {
          ...securityData,
          threatAnalysis: threatAnalysis,
          action: "blocked_high_threat",
        },
        "threat_blocked"
      );

      // Still redirect to thank you to not reveal detection
      res.redirect("/contact/thankyou");
      return;
    }

    // Send email (but flag for review if medium threat)
    const emailData = { name, email, message, subject };
    if (threatAnalysis.level === "medium") {
      emailData.securityFlag = `[SECURITY REVIEW REQUIRED - Score: ${threatAnalysis.score}]`;
    }

    await sendContactMail(emailData);

    // Log successful completion
    await logSecurityEvent(
      {
        ...securityData,
        threatAnalysis: threatAnalysis,
        processingResult: "success",
        emailSent: true,
      },
      "contact_success"
    );

    res.redirect("/contact/thankyou");
  } catch (err) {
    const errorData = captureSecurityData(req, {
      error: {
        message: err.message,
        stack: err.stack,
        name: err.name,
      },
      processingStep: "error_handling",
    });

    await logSecurityEvent(errorData, "contact_error");
    next(err);
  }
});

router.get("/contact", async (req, res) => {
  const securityData = captureSecurityData(req, {
    pageAccess: "contact_form",
    processingStep: "page_render",
  });

  await logSecurityEvent(securityData, "page_access");

  const context = {
    csrfToken: res.locals.csrfToken,
    title: "Contact",
    formAction: qualifyLink("/contact"),
    formMethod: "POST",
  };
  res.renderWithBaseContext("pages/contact.handlebars", context);
});

router.get("/contact/thankyou", async (req, res) => {
  const securityData = captureSecurityData(req, {
    pageAccess: "thankyou_page",
    processingStep: "page_render",
  });

  await logSecurityEvent(securityData, "thankyou_access");

  res.renderWithBaseContext("pages/thankyou.handlebars", {
    title: "Thank You",
  });
});

module.exports = router;