Seguridad y docs: fix SQLi en updatePost, middleware por segmentos exactos, cover-cache directo al upstream, JWT sin fallback hardcodeado, password sudo fuera de deploy/setup.sh; reescritura AGENTS.md y CLAUDE.md
This commit is contained in:
+5
-1
@@ -1,7 +1,11 @@
|
||||
import { SignJWT, jwtVerify } from "jose"
|
||||
import crypto from "crypto"
|
||||
|
||||
// Secreto de firma: JWT_SECRET explícito > WEB_PASSWORD > secreto efímero
|
||||
// aleatorio (modo dev sin contraseña — las sesiones no sobreviven restart,
|
||||
// pero no hay nada hardcodeado ni predecible en el código).
|
||||
const SECRET = new TextEncoder().encode(
|
||||
process.env.JWT_SECRET || process.env.WEB_PASSWORD || "worst-scan-web-dev-secret",
|
||||
process.env.JWT_SECRET || process.env.WEB_PASSWORD || crypto.randomBytes(32).toString("hex"),
|
||||
)
|
||||
|
||||
const COOKIE_NAME = "session"
|
||||
|
||||
+15
-3
@@ -1,5 +1,6 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { get } from "./settings"
|
||||
|
||||
const COVERS_DIR = process.env.COVERS_DIR || path.join(process.cwd(), "data", "covers")
|
||||
|
||||
@@ -20,10 +21,21 @@ function extFromContentType(ct: string | null): string {
|
||||
return exts[m[1]] || ".jpg"
|
||||
}
|
||||
|
||||
export async function cacheCover(gid: string, proxyUrl: string): Promise<string | null> {
|
||||
// Descarga la portada DIRECTAMENTE del upstream (API_BASE_URL), con la misma
|
||||
// API key que usa el resto de la app. NO pasa por el proxy /api/proxy/* (que el
|
||||
// middleware protege), evitando el redirect a /login que guardaba HTML como portada.
|
||||
export async function cacheCover(gid: string): Promise<string | null> {
|
||||
ensureDir()
|
||||
try {
|
||||
const res = await fetch(proxyUrl, { signal: AbortSignal.timeout(15000) })
|
||||
const apiBase = get("API_BASE_URL", "http://127.0.0.1:8080/api/v1")
|
||||
const apiKey = get("API_KEY")
|
||||
const headers: Record<string, string> = {}
|
||||
if (apiKey) headers["X-API-Key"] = apiKey
|
||||
|
||||
const res = await fetch(`${apiBase}/galleries/${gid}/cover`, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(15000),
|
||||
})
|
||||
if (!res.ok) return null
|
||||
|
||||
const buffer = Buffer.from(await res.arrayBuffer())
|
||||
@@ -61,4 +73,4 @@ export function getCoverContentType(gid: string): string {
|
||||
export function deleteCover(gid: string): void {
|
||||
const fp = getCoverPath(gid)
|
||||
if (fp) fs.unlinkSync(fp)
|
||||
}
|
||||
}
|
||||
+24
-9
@@ -145,20 +145,35 @@ export function createPost(input: PostInput): Post {
|
||||
return getPostById(result.lastInsertRowid as number)!
|
||||
}
|
||||
|
||||
export function updatePost(id: number, updates: Partial<PostInput & { published: number }>): Post | null {
|
||||
// Columnas permitidas en PATCH — whitelist estricta para impedir SQLi
|
||||
// por nombres de columna y el bypass de `published`/columnas internas.
|
||||
const UPDATE_FIELDS = new Set([
|
||||
"title",
|
||||
"title_jpn",
|
||||
"artist",
|
||||
"parody",
|
||||
"tags",
|
||||
"num_pages",
|
||||
"source",
|
||||
"cover_url",
|
||||
"url",
|
||||
"summary",
|
||||
"slug",
|
||||
])
|
||||
|
||||
export function updatePost(id: number, updates: Partial<PostInput>): Post | null {
|
||||
const d = getDb()
|
||||
const fields: string[] = []
|
||||
const values: Record<string, unknown> = { id }
|
||||
|
||||
for (const [k, v] of Object.entries(updates)) {
|
||||
if (v !== undefined) {
|
||||
if (k === "tags") {
|
||||
fields.push("tags = @tags")
|
||||
values.tags = JSON.stringify(v)
|
||||
} else {
|
||||
fields.push(`${k} = @${k}`)
|
||||
values[k] = v
|
||||
}
|
||||
if (v === undefined || !UPDATE_FIELDS.has(k)) continue
|
||||
if (k === "tags") {
|
||||
fields.push("tags = @tags")
|
||||
values.tags = JSON.stringify(v)
|
||||
} else {
|
||||
fields.push(`${k} = @${k}`)
|
||||
values[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ export async function pollOnce(): Promise<{ newPosts: number }> {
|
||||
}
|
||||
|
||||
const port = process.env.PORT || "3000"
|
||||
await cacheCover(gid, `http://127.0.0.1:${port}/api/proxy/galleries/${gid}/cover`)
|
||||
await cacheCover(gid)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user