mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
Fixes 4 high-severity findings from a comprehensive security audit: 1. Path traversal on ZIM file delete — resolve()+startsWith() containment 2. Path traversal on Map file delete — same pattern 3. Path traversal on docs read — same pattern (already used in rag_service) 4. SSRF on download endpoints — block private/internal IPs, require TLD Also adds assertNotPrivateUrl() to content update endpoints. Full audit report attached as admin/docs/security-audit-v1.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
112 lines
2.7 KiB
TypeScript
112 lines
2.7 KiB
TypeScript
import vine from '@vinejs/vine'
|
|
|
|
/**
|
|
* Checks whether a URL points to a private/internal network address.
|
|
* Used to prevent SSRF — the server should not fetch from localhost,
|
|
* private RFC1918 ranges, link-local, or cloud metadata endpoints.
|
|
*
|
|
* Throws an error if the URL is internal/private.
|
|
*/
|
|
export function assertNotPrivateUrl(urlString: string): void {
|
|
const parsed = new URL(urlString)
|
|
const hostname = parsed.hostname.toLowerCase()
|
|
|
|
const privatePatterns = [
|
|
/^localhost$/,
|
|
/^127\.\d+\.\d+\.\d+$/,
|
|
/^10\.\d+\.\d+\.\d+$/,
|
|
/^172\.(1[6-9]|2\d|3[01])\.\d+\.\d+$/,
|
|
/^192\.168\.\d+\.\d+$/,
|
|
/^169\.254\.\d+\.\d+$/, // Link-local / cloud metadata
|
|
/^0\.0\.0\.0$/,
|
|
/^\[::1\]$/,
|
|
/^\[?fe80:/i,
|
|
/^\[?fd[0-9a-f]{2}:/i, // Unique local IPv6
|
|
]
|
|
|
|
if (privatePatterns.some((re) => re.test(hostname))) {
|
|
throw new Error(`Download URL must not point to a private/internal address: ${hostname}`)
|
|
}
|
|
}
|
|
|
|
export const remoteDownloadValidator = vine.compile(
|
|
vine.object({
|
|
url: vine
|
|
.string()
|
|
.url()
|
|
.trim(),
|
|
})
|
|
)
|
|
|
|
export const remoteDownloadWithMetadataValidator = vine.compile(
|
|
vine.object({
|
|
url: vine
|
|
.string()
|
|
.url()
|
|
.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()
|
|
.trim()
|
|
.optional(),
|
|
})
|
|
)
|
|
|
|
export const filenameParamValidator = vine.compile(
|
|
vine.object({
|
|
params: vine.object({
|
|
filename: vine.string().trim().minLength(1).maxLength(4096),
|
|
}),
|
|
})
|
|
)
|
|
|
|
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().trim(),
|
|
})
|
|
|
|
export const applyContentUpdateValidator = vine.compile(resourceUpdateInfoBase)
|
|
|
|
export const applyAllContentUpdatesValidator = vine.compile(
|
|
vine.object({
|
|
updates: vine
|
|
.array(resourceUpdateInfoBase)
|
|
.minLength(1),
|
|
})
|
|
)
|