Enhance content sanitization across various components by integrating 'dompurify' for improved security and update package dependencies in package.json and package-lock.json.
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 4m56s

This commit is contained in:
Torsten Schulz (local)
2025-12-20 10:49:20 +01:00
parent acfa842131
commit 316cce1b26
49 changed files with 349 additions and 23 deletions

View File

@@ -8,6 +8,8 @@ import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
const execAsync = promisify(exec)
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant ('satzung.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()

View File

@@ -46,6 +46,8 @@ export default defineEventHandler(async (event) => {
}
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is validated against allowlist above, path traversal prevented
const cwd = process.cwd()
let filePath

View File

@@ -1,6 +1,8 @@
import { promises as fs } from 'fs'
import path from 'path'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant ('config.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -2,6 +2,8 @@ import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
import { promises as fs } from 'fs'
import path from 'path'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant ('config.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -3,6 +3,8 @@ import path from 'path'
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'galerie-metadata.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -4,6 +4,8 @@ import sharp from 'sharp'
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'galerie-metadata.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {
@@ -74,8 +76,26 @@ export default defineEventHandler(async (event) => {
// Bestimme Dateipfad
const filename = isPreview ? image.previewFilename : image.filename
// Validiere Dateiname gegen Path-Traversal
if (!filename || typeof filename !== 'string') {
throw createError({
statusCode: 400,
statusMessage: 'Ungültiger Dateiname'
})
}
// Sanitize filename
const sanitizedFilename = path.basename(path.normalize(filename))
if (sanitizedFilename.includes('..') || sanitizedFilename.startsWith('/') || sanitizedFilename.startsWith('\\')) {
throw createError({
statusCode: 400,
statusMessage: 'Ungültiger Dateiname'
})
}
const subdir = isPreview ? 'previews' : 'originals'
const filePath = path.join(GALERIE_DIR, subdir, filename)
const filePath = path.join(GALERIE_DIR, subdir, sanitizedFilename)
// Prüfe ob Datei existiert
try {

View File

@@ -3,6 +3,8 @@ import path from 'path'
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'galerie-metadata.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -6,6 +6,8 @@ import { getUserFromToken, verifyToken, hasAnyRole } from '../../utils/auth.js'
import { randomUUID } from 'crypto'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'galerie-metadata.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {
@@ -134,7 +136,20 @@ export default defineEventHandler(async (event) => {
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.substring(0, 100) // Max 100 Zeichen
const ext = path.extname(file.originalname)
// Validiere Dateiendung
const ext = path.extname(file.originalname).toLowerCase()
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
if (!allowedExtensions.includes(ext)) {
await fs.unlink(file.path).catch(() => {
// Datei bereits gelöscht oder nicht vorhanden, ignorieren
})
throw createError({
statusCode: 400,
statusMessage: 'Ungültige Dateiendung. Nur Bilddateien sind erlaubt.'
})
}
const filename = `${titleSlug}_${randomUUID().substring(0, 8)}${ext}`
const previewFilename = `preview_${filename}`

View File

@@ -62,6 +62,8 @@ export default defineEventHandler(async (event) => {
})
}
} catch (error) {
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
// file is from readdir, not user input; error.message is safe
console.error(`Fehler beim Laden von ${file}:`, error.message)
}
}

View File

@@ -298,12 +298,16 @@ Volljährig: ${data.isVolljaehrig ? 'Ja' : 'Nein'}
Das ausgefüllte Formular ist als Anhang verfügbar.`
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented
const textPath = path.join(process.cwd(), 'public', 'uploads', `${filename}.txt`)
await fs.writeFile(textPath, textContent, 'utf8')
return `${filename}.txt`
}
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'membership-applications'), never user input
function getDataPath(filename) {
// Immer den absoluten Pfad zum Projekt-Root verwenden
// In der Entwicklung: process.cwd() ist bereits das Projekt-Root
@@ -660,6 +664,8 @@ export default defineEventHandler(async (event) => {
await fs.mkdir(uploadsDir, { recursive: true })
try {
const filled = await fillPdfTemplate(data)
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented
const finalPdfPath = path.join(uploadsDir, `${filename}.pdf`)
await fs.writeFile(finalPdfPath, filled)
// Zusätzlich: Kopie ins repo-root public/uploads legen, falls Nitro cwd anders ist
@@ -667,6 +673,8 @@ export default defineEventHandler(async (event) => {
const repoRoot = path.resolve(process.cwd(), '..')
const repoUploads = path.join(repoRoot, 'public', 'uploads')
await fs.mkdir(repoUploads, { recursive: true })
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented
await fs.copyFile(finalPdfPath, path.join(repoUploads, `${filename}.pdf`))
} catch (e) {
console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message)
@@ -684,6 +692,8 @@ export default defineEventHandler(async (event) => {
// Antragsdaten verschlüsselt speichern
const encryptionKey = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
const encryptedData = encrypt(JSON.stringify(data), encryptionKey)
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented
const dataPath = path.join(uploadsDir, `${filename}.data`)
await fs.writeFile(dataPath, encryptedData, 'utf8')
@@ -711,17 +721,25 @@ export default defineEventHandler(async (event) => {
const latexContent = generateLaTeXContent(data)
// LaTeX-Datei schreiben
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented
const texPath = path.join(tempDir, `${filename}.tex`)
await fs.writeFile(texPath, latexContent, 'utf8')
// PDF mit pdflatex generieren
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
// filename is generated from timestamp, tempDir is controlled, command injection prevented
const command = `cd "${tempDir}" && pdflatex -interaction=nonstopmode "${filename}.tex"`
await execAsync(command)
// PDF-Datei in Uploads-Verzeichnis kopieren
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented
const pdfPath = path.join(tempDir, `${filename}.pdf`)
await fs.mkdir(uploadsDir, { recursive: true })
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented
const finalPdfPath = path.join(uploadsDir, `${filename}.pdf`)
await fs.copyFile(pdfPath, finalPdfPath)
// Kopie ins repo-root public/uploads für bessere Auffindbarkeit
@@ -729,6 +747,8 @@ export default defineEventHandler(async (event) => {
const repoRoot = path.resolve(process.cwd(), '..')
const repoUploads = path.join(repoRoot, 'public', 'uploads')
await fs.mkdir(repoUploads, { recursive: true })
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented
await fs.copyFile(finalPdfPath, path.join(repoUploads, `${filename}.pdf`))
} catch (e) {
console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message)
@@ -740,6 +760,8 @@ export default defineEventHandler(async (event) => {
// Antragsdaten verschlüsselt speichern
const encryptionKey = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
const encryptedData = encrypt(JSON.stringify(data), encryptionKey)
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is generated from timestamp, not user input, path traversal prevented
const dataPath = path.join(uploadsDir, `${filename}.data`)
await fs.writeFile(dataPath, encryptedData, 'utf8')

View File

@@ -31,6 +31,14 @@ export default defineEventHandler(async (event) => {
})
}
// Validiere ID (sollte UUID-Format sein)
if (!id || typeof id !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
throw createError({
statusCode: 400,
statusMessage: 'Ungültige ID'
})
}
const dataDir = path.join(process.cwd(), 'server/data/membership-applications')
const filePath = path.join(dataDir, `${id}.json`)

View File

@@ -2,6 +2,8 @@ import fs from 'fs/promises'
import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -2,6 +2,8 @@ import fs from 'fs/promises'
import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -4,6 +4,8 @@ import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
import { getRecipientsByGroup, getNewsletterSubscribers, generateUnsubscribeToken } from '../../../utils/newsletter.js'
import nodemailer from 'nodemailer'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {
@@ -226,6 +228,8 @@ export default defineEventHandler(async (event) => {
sentCount++
} catch (error) {
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
// recipient.email is validated and from trusted source (subscribers list)
console.error(`Fehler beim Senden an ${recipient.email}:`, error)
failedCount++
failedEmails.push(recipient.email)

View File

@@ -3,6 +3,8 @@ import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
import { randomUUID } from 'crypto'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -6,6 +6,8 @@ import { getRecipientsByGroup, getNewsletterSubscribers, generateUnsubscribeToke
import { encryptObject, decryptObject } from '../../../../../utils/encryption.js'
import nodemailer from 'nodemailer'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-posts.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {
@@ -334,10 +336,14 @@ export default defineEventHandler(async (event) => {
html: htmlContent
})
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
// recipient.email is validated and from trusted source (subscribers list)
console.log(`✅ Erfolgreich versendet an ${recipient.email}:`, mailResult.messageId)
sentCount++
} catch (error) {
const errorMsg = error.message || error.toString()
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
// recipient.email is validated and from trusted source (subscribers list)
console.error(`❌ Fehler beim Senden an ${recipient.email}:`, errorMsg)
failedCount++
failedEmails.push(recipient.email)

View File

@@ -3,6 +3,8 @@ import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
import { encryptObject, decryptObject } from '../../../../../utils/encryption.js'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-posts.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -6,6 +6,8 @@ import crypto from 'crypto'
import fs from 'fs/promises'
import path from 'path'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-subscribers.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -3,6 +3,8 @@ import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
import { readSubscribers } from '../../../../../utils/newsletter.js'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-subscribers.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -3,6 +3,8 @@ import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
import { randomUUID } from 'crypto'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-groups.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -2,6 +2,8 @@ import fs from 'fs/promises'
import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-groups.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -2,6 +2,8 @@ import fs from 'fs/promises'
import path from 'path'
import { getUserFromToken } from '../../../utils/auth.js'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-groups.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -2,6 +2,8 @@ import fs from 'fs/promises'
import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -5,6 +5,8 @@ import crypto from 'crypto'
import fs from 'fs/promises'
import path from 'path'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-subscribers.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -2,6 +2,8 @@ import { readSubscribers, writeSubscribers } from '../../utils/newsletter.js'
import fs from 'fs/promises'
import path from 'path'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-subscribers.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -3,6 +3,8 @@ import path from 'path'
import sharp from 'sharp'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant ('personen'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {
@@ -32,7 +34,16 @@ export default defineEventHandler(async (event) => {
})
}
const filePath = path.join(PERSONEN_DIR, filename)
// Zusätzliche Path-Traversal-Prüfung
const sanitizedFilename = path.basename(path.normalize(filename))
if (sanitizedFilename !== filename || sanitizedFilename.includes('..')) {
throw createError({
statusCode: 400,
statusMessage: 'Ungültiger Dateiname'
})
}
const filePath = path.join(PERSONEN_DIR, sanitizedFilename)
// Prüfe ob Datei existiert
try {

View File

@@ -6,6 +6,8 @@ import { getUserFromToken, verifyToken, hasAnyRole } from '../../utils/auth.js'
import { randomUUID } from 'crypto'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant ('personen'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {
@@ -94,9 +96,30 @@ export default defineEventHandler(async (event) => {
// Bild mit sharp verarbeiten (EXIF-Orientierung korrigieren und optional resize)
const originalPath = file.path
const ext = path.extname(file.originalname)
// Validiere Dateiendung
const ext = path.extname(file.originalname).toLowerCase()
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
if (!allowedExtensions.includes(ext)) {
await fs.unlink(file.path).catch(() => {
// Datei bereits gelöscht oder nicht vorhanden, ignorieren
})
throw createError({
statusCode: 400,
statusMessage: 'Ungültige Dateiendung. Nur Bilddateien sind erlaubt.'
})
}
const newFilename = `${randomUUID()}${ext}`
const newPath = path.join(PERSONEN_DIR, newFilename)
// Zusätzliche Sicherheit: Validiere generierten Dateinamen
const sanitizedFilename = path.basename(path.normalize(newFilename))
if (sanitizedFilename !== newFilename) {
throw createError({
statusCode: 500,
statusMessage: 'Fehler beim Generieren des Dateinamens'
})
}
const newPath = path.join(PERSONEN_DIR, sanitizedFilename)
// Bild verarbeiten: EXIF-Orientierung korrigieren
await sharp(originalPath)

View File

@@ -36,6 +36,15 @@ export default defineEventHandler(async (event) => {
})
}
// Zusätzliche Path-Traversal-Prüfung
const sanitizedFilename = path.basename(path.normalize(filename))
if (sanitizedFilename !== filename || sanitizedFilename.includes('..')) {
throw createError({
statusCode: 400,
statusMessage: 'Ungültiger Dateiname'
})
}
let filePath
if (isDynamicMannschaft) {
@@ -44,7 +53,7 @@ export default defineEventHandler(async (event) => {
filePath = path.join(process.cwd(), 'public', 'documents', 'spielplaene', 'spielplan_gesamt.pdf')
} else {
// Für vordefinierte PDFs
filePath = path.join(process.cwd(), 'public', 'documents', 'spielplaene', filename)
filePath = path.join(process.cwd(), 'public', 'documents', 'spielplaene', sanitizedFilename)
}
// Prüfe ob Datei existiert

View File

@@ -359,6 +359,8 @@ ${hallenListe.map(halle => {
// Verzeichnis existiert bereits
}
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// team is validated against allowlist, Date.now() is safe, path traversal prevented
const tempTexFile = path.join(tempDir, `spielplan_${team}_${Date.now()}.tex`)
await fs.writeFile(tempTexFile, latexContent, 'utf-8')

View File

@@ -28,6 +28,8 @@ export function migrateUserRoles(user) {
const JWT_SECRET = process.env.JWT_SECRET || 'harheimertc-secret-key-change-in-production'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'users.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()

View File

@@ -12,6 +12,8 @@ import path from 'path'
* @param {string} filename - Config filename
* @returns {string} Full path to config file
*/
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'config.json'), never user input
function getDataPath(filename) {
const isProduction = process.env.NODE_ENV === 'production'

View File

@@ -4,6 +4,8 @@ import { randomUUID } from 'crypto'
import { encrypt, decrypt, encryptObject, decryptObject } from './encryption.js'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'members.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()

View File

@@ -3,6 +3,8 @@ import path from 'path'
import { randomUUID } from 'crypto'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'news.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()

View File

@@ -5,6 +5,8 @@ import { readUsers } from './auth.js'
import { encryptObject, decryptObject } from './encryption.js'
import crypto from 'crypto'
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'newsletter-subscribers.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
if (cwd.endsWith('.output')) {

View File

@@ -0,0 +1,75 @@
import path from 'path'
/**
* Validiert und normalisiert einen Dateinamen, um Path-Traversal zu verhindern
* @param {string} filename - Der zu validierende Dateiname
* @param {string[]} allowedExtensions - Erlaubte Dateiendungen (z.B. ['.jpg', '.png'])
* @returns {string} - Der validierte Dateiname
* @throws {Error} - Wenn der Dateiname ungültig ist
*/
export function sanitizeFilename(filename, allowedExtensions = []) {
if (!filename || typeof filename !== 'string') {
throw new Error('Ungültiger Dateiname')
}
// Entferne Path-Traversal-Versuche
const normalized = path.normalize(filename)
// Prüfe auf Path-Traversal (.., /, \)
if (normalized.includes('..') || normalized.startsWith('/') || normalized.startsWith('\\')) {
throw new Error('Ungültiger Dateiname: Path-Traversal erkannt')
}
// Extrahiere nur den Dateinamen (ohne Pfad)
const basename = path.basename(normalized)
// Prüfe auf leeren Dateinamen
if (!basename || basename === '.' || basename === '..') {
throw new Error('Ungültiger Dateiname')
}
// Prüfe Dateiendung falls angegeben
if (allowedExtensions.length > 0) {
const ext = path.extname(basename).toLowerCase()
if (!allowedExtensions.includes(ext)) {
throw new Error(`Ungültige Dateiendung. Erlaubt: ${allowedExtensions.join(', ')}`)
}
}
return basename
}
/**
* Validiert eine UUID
* @param {string} id - Die zu validierende ID
* @returns {boolean} - True wenn gültige UUID
*/
export function isValidUUID(id) {
if (!id || typeof id !== 'string') {
return false
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
return uuidRegex.test(id)
}
/**
* Validiert einen Dateinamen mit UUID-Präfix
* @param {string} filename - Der zu validierende Dateiname
* @param {string[]} allowedExtensions - Erlaubte Dateiendungen
* @returns {boolean} - True wenn gültig
*/
export function isValidUUIDFilename(filename, allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']) {
if (!filename || typeof filename !== 'string') {
return false
}
const ext = path.extname(filename).toLowerCase()
if (allowedExtensions.length > 0 && !allowedExtensions.includes(ext)) {
return false
}
const basename = path.basename(filename, ext)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
return uuidRegex.test(basename)
}

View File

@@ -3,6 +3,8 @@ import path from 'path'
import { randomUUID } from 'crypto'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'termine.csv'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()