feat: implementar encabezados de seguridad mejorados y política de contenido para solicitudes
This commit is contained in:
@@ -114,6 +114,45 @@ function applySecurityHeaders(base: Record<string, string> = {}) {
|
|||||||
...base,
|
...base,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function buildBaseCsp(frameAncestors: string = "'self'") {
|
||||||
|
// Use a mild CSP; add frame-ancestors dynamically per request.
|
||||||
|
return (
|
||||||
|
"default-src 'self'; " +
|
||||||
|
"img-src 'self' data: https:; " +
|
||||||
|
"style-src 'self' 'unsafe-inline' https:; " +
|
||||||
|
"script-src 'self' 'unsafe-inline' https:; " +
|
||||||
|
"font-src 'self' https: data:; " +
|
||||||
|
"frame-src 'self' https://ko-fi.com https://*.ko-fi.com; " +
|
||||||
|
"child-src 'self' https://ko-fi.com https://*.ko-fi.com; " +
|
||||||
|
`frame-ancestors ${frameAncestors}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySecurityHeadersForRequest(
|
||||||
|
req: IncomingMessage,
|
||||||
|
base: Record<string, string> = {}
|
||||||
|
) {
|
||||||
|
const host = ((req.headers.host as string) || "").toLowerCase();
|
||||||
|
const isDocsHost =
|
||||||
|
host === "docs.amayo.dev" || host.endsWith(".docs.amayo.dev");
|
||||||
|
|
||||||
|
// Allow embedding only from https://top.gg for docs.amayo.dev; otherwise, self only and keep XFO deny.
|
||||||
|
const csp = isDocsHost
|
||||||
|
? buildBaseCsp("'self' https://top.gg")
|
||||||
|
: buildBaseCsp("'self'");
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"Referrer-Policy": "no-referrer",
|
||||||
|
// X-Frame-Options is omitted for docs.amayo.dev to rely on CSP frame-ancestors allowing only top.gg
|
||||||
|
...(isDocsHost ? {} : { "X-Frame-Options": "DENY" }),
|
||||||
|
"Content-Security-Policy": csp,
|
||||||
|
...base,
|
||||||
|
};
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
function computeEtag(buf: Buffer): string {
|
function computeEtag(buf: Buffer): string {
|
||||||
// Weak ETag derived from content sha1 and length
|
// Weak ETag derived from content sha1 and length
|
||||||
@@ -174,7 +213,7 @@ const sendResponse = async (
|
|||||||
if (inm && inm === etag) {
|
if (inm && inm === etag) {
|
||||||
res.writeHead(
|
res.writeHead(
|
||||||
304,
|
304,
|
||||||
applySecurityHeaders({
|
applySecurityHeadersForRequest(req, {
|
||||||
ETag: etag,
|
ETag: etag,
|
||||||
"Cache-Control": cacheControl,
|
"Cache-Control": cacheControl,
|
||||||
...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}),
|
...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}),
|
||||||
@@ -202,7 +241,7 @@ const sendResponse = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.writeHead(statusCode, applySecurityHeaders(headers));
|
res.writeHead(statusCode, applySecurityHeadersForRequest(req, headers));
|
||||||
res.end(body);
|
res.end(body);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -238,7 +277,10 @@ const renderTemplate = async (
|
|||||||
if (inm && inm === etag) {
|
if (inm && inm === etag) {
|
||||||
res.writeHead(
|
res.writeHead(
|
||||||
304,
|
304,
|
||||||
applySecurityHeaders({ ETag: etag, "Cache-Control": "no-cache" })
|
applySecurityHeadersForRequest(req, {
|
||||||
|
ETag: etag,
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
})
|
||||||
);
|
);
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
@@ -261,7 +303,7 @@ const renderTemplate = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.writeHead(statusCode, applySecurityHeaders(headers));
|
res.writeHead(statusCode, applySecurityHeadersForRequest(req, headers));
|
||||||
res.end(respBody);
|
res.end(respBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,7 +331,7 @@ export const server = createServer(
|
|||||||
const robots = "User-agent: *\nAllow: /\n"; // change to Disallow: / if you want to discourage polite crawlers
|
const robots = "User-agent: *\nAllow: /\n"; // change to Disallow: / if you want to discourage polite crawlers
|
||||||
res.writeHead(
|
res.writeHead(
|
||||||
200,
|
200,
|
||||||
applySecurityHeaders({
|
applySecurityHeadersForRequest(req, {
|
||||||
"Content-Type": "text/plain; charset=utf-8",
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
"Cache-Control": "public, max-age=86400",
|
"Cache-Control": "public, max-age=86400",
|
||||||
})
|
})
|
||||||
@@ -301,7 +343,7 @@ export const server = createServer(
|
|||||||
if (isSuspiciousPath(url.pathname)) {
|
if (isSuspiciousPath(url.pathname)) {
|
||||||
// Hard block known-bad keyword probes
|
// Hard block known-bad keyword probes
|
||||||
if (BLOCKED_PATTERNS.some((re) => re.test(url.pathname))) {
|
if (BLOCKED_PATTERNS.some((re) => re.test(url.pathname))) {
|
||||||
const headers = applySecurityHeaders({
|
const headers = applySecurityHeadersForRequest(req, {
|
||||||
"Content-Type": "text/plain; charset=utf-8",
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
});
|
});
|
||||||
res.writeHead(403, headers);
|
res.writeHead(403, headers);
|
||||||
@@ -310,7 +352,7 @@ export const server = createServer(
|
|||||||
// Rate limit repetitive suspicious hits per IP
|
// Rate limit repetitive suspicious hits per IP
|
||||||
const rate = hitSuspicious(clientIp);
|
const rate = hitSuspicious(clientIp);
|
||||||
if (!rate.allowed) {
|
if (!rate.allowed) {
|
||||||
const headers = applySecurityHeaders({
|
const headers = applySecurityHeadersForRequest(req, {
|
||||||
"Content-Type": "text/plain; charset=utf-8",
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
"Retry-After": String(Math.ceil(rate.resetIn / 1000)),
|
"Retry-After": String(Math.ceil(rate.resetIn / 1000)),
|
||||||
"X-RateLimit-Limit": String(RATE_MAX_SUSPICIOUS),
|
"X-RateLimit-Limit": String(RATE_MAX_SUSPICIOUS),
|
||||||
@@ -360,7 +402,7 @@ export const server = createServer(
|
|||||||
} catch {
|
} catch {
|
||||||
res.writeHead(
|
res.writeHead(
|
||||||
404,
|
404,
|
||||||
applySecurityHeaders({
|
applySecurityHeadersForRequest(req, {
|
||||||
"Content-Type": "text/plain; charset=utf-8",
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -373,7 +415,7 @@ export const server = createServer(
|
|||||||
console.error("[Server] Error al servir archivo:", error);
|
console.error("[Server] Error al servir archivo:", error);
|
||||||
res.writeHead(
|
res.writeHead(
|
||||||
500,
|
500,
|
||||||
applySecurityHeaders({
|
applySecurityHeadersForRequest(req, {
|
||||||
"Content-Type": "text/plain; charset=utf-8",
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -384,7 +426,9 @@ export const server = createServer(
|
|||||||
console.error("[Server] Error inesperado:", error);
|
console.error("[Server] Error inesperado:", error);
|
||||||
res.writeHead(
|
res.writeHead(
|
||||||
500,
|
500,
|
||||||
applySecurityHeaders({ "Content-Type": "text/plain; charset=utf-8" })
|
applySecurityHeadersForRequest(req, {
|
||||||
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
|
})
|
||||||
);
|
);
|
||||||
res.end("500 - Error interno");
|
res.end("500 - Error interno");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user