mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-29 04:59:26 +02:00
- CORS: restrict origin from wildcard '*' to app URL from env (prevents cross-origin requests from arbitrary sites) - Sessions: enable @adonisjs/session with cookie store and httpOnly/secure cookie flags; uncomment session middleware in kernel.ts - CSRF: enable shield CSRF protection (requires sessions); uses XSRF-TOKEN cookie mechanism compatible with Inertia.js/Axios; exempts /api/health and SSE transmit endpoints - CSP: enable Content Security Policy with restrictive directives (no object-src, no frame-src, self-only script/style/connect/font) - HSTS: enable HTTP Strict Transport Security in production only - Path traversal: tighten filenameParamValidator to block /, \, .., and shell special characters; reduce max length from 4096 to 255 - env: add URL to .env.example; uncomment SESSION_DRIVER validation in env.ts https://claude.ai/code/session_01WfRC4tDeYprykhMrg4PxX6
119 lines
3.2 KiB
TypeScript
119 lines
3.2 KiB
TypeScript
import vine from '@vinejs/vine'
|
|
|
|
/**
|
|
* Checks whether a URL points to a loopback or link-local address.
|
|
* Used to prevent SSRF — the server should not fetch from localhost
|
|
* or link-local/metadata endpoints (e.g. cloud instance metadata at 169.254.x.x).
|
|
*
|
|
* RFC1918 private ranges (10.x, 172.16-31.x, 192.168.x) are intentionally
|
|
* ALLOWED because NOMAD is a LAN appliance and users may host content
|
|
* mirrors on their local network.
|
|
*
|
|
* Throws an error if the URL is a loopback or link-local address.
|
|
*/
|
|
export function assertNotPrivateUrl(urlString: string): void {
|
|
const parsed = new URL(urlString)
|
|
const hostname = parsed.hostname.toLowerCase()
|
|
|
|
const blockedPatterns = [
|
|
/^localhost$/,
|
|
/^127\.\d+\.\d+\.\d+$/,
|
|
/^0\.0\.0\.0$/,
|
|
/^169\.254\.\d+\.\d+$/, // Link-local / cloud metadata
|
|
/^\[::1\]$/,
|
|
/^\[?fe80:/i, // IPv6 link-local
|
|
]
|
|
|
|
if (blockedPatterns.some((re) => re.test(hostname))) {
|
|
throw new Error(`Download URL must not point to a loopback or link-local address: ${hostname}`)
|
|
}
|
|
}
|
|
|
|
export const remoteDownloadValidator = vine.compile(
|
|
vine.object({
|
|
url: vine
|
|
.string()
|
|
.url({ require_tld: false }) // Allow LAN URLs (e.g. http://my-nas:8080/file.zim)
|
|
.trim(),
|
|
})
|
|
)
|
|
|
|
export const remoteDownloadWithMetadataValidator = vine.compile(
|
|
vine.object({
|
|
url: vine
|
|
.string()
|
|
.url({ require_tld: false }) // Allow LAN URLs
|
|
.trim(),
|
|
metadata: vine
|
|
.object({
|
|
title: vine.string().trim().minLength(1),
|
|
summary: vine.string().trim().optional(),
|
|
author: vine.string().trim().optional(),
|
|
size_bytes: vine.number().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
)
|
|
|
|
export const remoteDownloadValidatorOptional = vine.compile(
|
|
vine.object({
|
|
url: vine
|
|
.string()
|
|
.url({ require_tld: false }) // Allow LAN URLs
|
|
.trim()
|
|
.optional(),
|
|
})
|
|
)
|
|
|
|
export const filenameParamValidator = vine.compile(
|
|
vine.object({
|
|
params: vine.object({
|
|
filename: vine
|
|
.string()
|
|
.trim()
|
|
.minLength(1)
|
|
.maxLength(255)
|
|
.regex(/^[^/\\]*$/) // Disallow path separators to prevent traversal
|
|
.regex(/^(?!\.\.)/) // Disallow leading double-dots
|
|
.regex(/^[^<>:"|?*\x00-\x1f]+$/), // Disallow shell/filesystem special chars
|
|
}),
|
|
})
|
|
)
|
|
|
|
export const downloadCollectionValidator = vine.compile(
|
|
vine.object({
|
|
slug: vine.string(),
|
|
})
|
|
)
|
|
|
|
export const downloadCategoryTierValidator = vine.compile(
|
|
vine.object({
|
|
categorySlug: vine.string().trim().minLength(1),
|
|
tierSlug: vine.string().trim().minLength(1),
|
|
})
|
|
)
|
|
|
|
export const selectWikipediaValidator = vine.compile(
|
|
vine.object({
|
|
optionId: vine.string().trim().minLength(1),
|
|
})
|
|
)
|
|
|
|
const resourceUpdateInfoBase = vine.object({
|
|
resource_id: vine.string().trim().minLength(1),
|
|
resource_type: vine.enum(['zim', 'map'] as const),
|
|
installed_version: vine.string().trim(),
|
|
latest_version: vine.string().trim().minLength(1),
|
|
download_url: vine.string().url({ require_tld: false }).trim(),
|
|
})
|
|
|
|
export const applyContentUpdateValidator = vine.compile(resourceUpdateInfoBase)
|
|
|
|
export const applyAllContentUpdatesValidator = vine.compile(
|
|
vine.object({
|
|
updates: vine
|
|
.array(resourceUpdateInfoBase)
|
|
.minLength(1),
|
|
})
|
|
)
|