Merge branch 'fix/alias-route-bot-traffic-oom' into main

This commit is contained in:
2026-08-27 09:35:33 +02:00
4 changed files with 35 additions and 4 deletions
+8
View File
@@ -7,6 +7,14 @@ and this project follows [Semantic Versioning](https://semver.org/).
## [Unreleased] ## [Unreleased]
### Fixed
- The public event-alias route (`/:redirectUrl`, used for short links like `/camp-2025`) is a catch-all matching any unmatched top-level path, so every bot/scanner probe for a nonexistent page (`/wp-login.php`, `/.env`, etc.) was hitting the backend and firing a live database query. Both the frontend route and the `GET /api/events/by-alias/:redirectUrl` endpoint now reject anything that isn't a plausible alias (letters/numbers/hyphens/underscores) before touching the database, instead of forwarding scanner noise straight through — this traffic pattern could exhaust the database connection pool and take the server down under load.
### Added
- PM2 (`ecosystem.config.js`) now restarts either process if it exceeds 500MB of memory, instead of letting an unbounded leak run until the OS OOM-kills it.
## [1.9.4] - 2026-08-26 ## [1.9.4] - 2026-08-26
### Added ### Added
@@ -1696,9 +1696,20 @@ const scheduleWhatsappEventAttendees = async (req, res) => {
* @route GET /api/events/by-alias/:redirectUrl * @route GET /api/events/by-alias/:redirectUrl
* @access Public * @access Public
*/ */
// Aliases are admin-set slugs (e.g. "camp-2025") — see the event wizard's "URL
// Alias" field. This endpoint is public and also the target of the frontend's
// catch-all [redirectUrl] route, so it's what every bot/scanner probe hitting
// an unmatched top-level path (/wp-login.php, /.env, etc.) ends up calling.
// Rejecting non-slug-shaped values here skips a DB round-trip for that traffic.
const VALID_ALIAS = /^[a-zA-Z0-9_-]{1,100}$/;
const getEventByAlias = async (req, res) => { const getEventByAlias = async (req, res) => {
const { redirectUrl } = req.params; const { redirectUrl } = req.params;
if (!VALID_ALIAS.test(redirectUrl)) {
return res.status(404).json({ message: 'Event not found' });
}
try { try {
const event = await prisma.event.findFirst({ const event = await prisma.event.findFirst({
where: { where: {
+2
View File
@@ -5,6 +5,7 @@ module.exports = {
cwd: __dirname + '/backend', cwd: __dirname + '/backend',
script: 'src/index.js', script: 'src/index.js',
env: { NODE_ENV: 'production' }, env: { NODE_ENV: 'production' },
max_memory_restart: '500M',
}, },
{ {
name: 'hope-events-frontend', name: 'hope-events-frontend',
@@ -12,6 +13,7 @@ module.exports = {
script: 'npm', script: 'npm',
args: 'start -- -p 3000', args: 'start -- -p 3000',
env: { NODE_ENV: 'production' }, env: { NODE_ENV: 'production' },
max_memory_restart: '500M',
}, },
], ],
}; };
+14 -4
View File
@@ -3,15 +3,25 @@ import { apiFetch } from "@/lib/api";
export const revalidate = 60; export const revalidate = 60;
// Event aliases are admin-set slugs (e.g. "camp-2025", "movie-night") — see the
// "URL Alias" field in the event wizard. This catch-all route matches *any*
// unmatched top-level path, so it's also what every bot/scanner probe hits
// (/wp-login.php, /.env, /xmlrpc.php, etc.). Rejecting anything that isn't a
// plausible slug here skips a live DB query for that background noise instead
// of forwarding it straight to the backend.
const VALID_ALIAS = /^[a-zA-Z0-9_-]{1,100}$/;
export default async function EventRedirectPage({ params }: { params: Promise<{ redirectUrl: string }> }) { export default async function EventRedirectPage({ params }: { params: Promise<{ redirectUrl: string }> }) {
const { redirectUrl } = await params; const { redirectUrl } = await params;
let event: any = null; let event: any = null;
try { if (VALID_ALIAS.test(redirectUrl)) {
event = await apiFetch<any>(`/api/events/by-alias/${redirectUrl}`); try {
} catch (error) { event = await apiFetch<any>(`/api/events/by-alias/${redirectUrl}`);
console.error("Failed to fetch event:", error); } catch (error) {
console.error("Failed to fetch event:", error);
}
} }
if (!event || event.message?.toLowerCase().includes("not found")) { if (!event || event.message?.toLowerCase().includes("not found")) {