Session 45: support email, audit fixes (template validation, content-type, admin auth, waitUntil)
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 2m20s
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 2m20s
- Added support@docfast.dev to footer, impressum, terms, landing page, openapi.json - Fixed audit #6: Template render validates required fields (400 on missing) - Fixed audit #7: Content-Type check on markdown/URL routes (415) - Fixed audit #11: /v1/usage and /v1/concurrency now require ADMIN_API_KEY - Fixed audit Critical #3: URL convert uses domcontentloaded instead of networkidle0
This commit is contained in:
parent
8a86e34f91
commit
59cc8f3d0e
22 changed files with 166 additions and 61 deletions
37
dist/services/browser.js
vendored
37
dist/services/browser.js
vendored
|
|
@ -224,10 +224,45 @@ export async function renderUrlPdf(url, options = {}) {
|
|||
const { page, instance } = await acquirePage();
|
||||
try {
|
||||
await page.setJavaScriptEnabled(false);
|
||||
// Pin DNS resolution to prevent DNS rebinding SSRF attacks
|
||||
if (options.hostResolverRules) {
|
||||
const client = await page.createCDPSession();
|
||||
// Use Chrome DevTools Protocol to set host resolver rules per-page
|
||||
await client.send("Network.enable");
|
||||
// Extract hostname and IP from rules like "MAP hostname ip"
|
||||
const match = options.hostResolverRules.match(/^MAP\s+(\S+)\s+(\S+)$/);
|
||||
if (match) {
|
||||
const [, hostname, ip] = match;
|
||||
await page.setRequestInterception(true);
|
||||
page.on("request", (request) => {
|
||||
const reqUrl = new URL(request.url());
|
||||
if (reqUrl.hostname === hostname) {
|
||||
// For HTTP, rewrite to IP with Host header
|
||||
if (reqUrl.protocol === "http:") {
|
||||
reqUrl.hostname = ip;
|
||||
request.continue({
|
||||
url: reqUrl.toString(),
|
||||
headers: { ...request.headers(), host: hostname },
|
||||
});
|
||||
}
|
||||
else {
|
||||
// For HTTPS, we can't easily swap the IP without cert issues
|
||||
// But we've already validated the IP, and the short window makes rebinding unlikely
|
||||
// Combined with JS disabled, this is sufficient mitigation
|
||||
request.continue();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Block any requests to other hosts (prevent redirects to internal IPs)
|
||||
request.abort("blockedbyclient");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const result = await Promise.race([
|
||||
(async () => {
|
||||
await page.goto(url, {
|
||||
waitUntil: options.waitUntil || "networkidle0",
|
||||
waitUntil: options.waitUntil || "domcontentloaded",
|
||||
timeout: 30_000,
|
||||
});
|
||||
const pdf = await page.pdf({
|
||||
|
|
|
|||
7
dist/services/verification.js
vendored
7
dist/services/verification.js
vendored
|
|
@ -1,4 +1,4 @@
|
|||
import { randomBytes, randomInt } from "crypto";
|
||||
import { randomBytes, randomInt, timingSafeEqual } from "crypto";
|
||||
import logger from "./logger.js";
|
||||
import pool from "./db.js";
|
||||
const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000;
|
||||
|
|
@ -87,7 +87,10 @@ export async function verifyCode(email, code) {
|
|||
return { status: "max_attempts" };
|
||||
}
|
||||
await pool.query("UPDATE pending_verifications SET attempts = attempts + 1 WHERE email = $1", [cleanEmail]);
|
||||
if (pending.code !== code) {
|
||||
const a = Buffer.from(pending.code, "utf8");
|
||||
const b = Buffer.from(code, "utf8");
|
||||
const codeMatch = a.length === b.length && timingSafeEqual(a, b);
|
||||
if (!codeMatch) {
|
||||
return { status: "invalid" };
|
||||
}
|
||||
await pool.query("DELETE FROM pending_verifications WHERE email = $1", [cleanEmail]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue