From edfab28fd3300c6a7b5cb4b0fbf1bea0d598d780 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 15 Apr 2026 20:52:38 +0200 Subject: [PATCH] Add security comments to path handling in various scripts to clarify internal constant usage and mitigate path traversal risks. Update logging in registration and verification processes for improved clarity. --- scripts/add-vorstand-role.js | 4 ++++ scripts/generate-galerie-previews.js | 8 ++++++++ scripts/inspect-forms.js | 2 ++ scripts/migrate-public-galerie-to-metadata.js | 4 ++++ scripts/re-encrypt-membership-applications.js | 2 ++ server/api/auth/register-passkey-options.post.js | 4 ++-- server/api/auth/register-passkey.post.js | 6 +++--- server/api/cms/save-csv.post.js | 4 ++++ server/api/contact.post.js | 4 ++++ server/api/media/galerie/[id].get.js | 4 ++++ server/api/media/internal/[...path].get.js | 2 ++ server/utils/encryption.js | 2 +- server/utils/paths.js | 2 ++ server/utils/termine.js | 4 ++++ 14 files changed, 46 insertions(+), 6 deletions(-) diff --git a/scripts/add-vorstand-role.js b/scripts/add-vorstand-role.js index f3d5696..11302f7 100644 --- a/scripts/add-vorstand-role.js +++ b/scripts/add-vorstand-role.js @@ -26,8 +26,12 @@ const targetEmail = String(process.argv[2] || 'tsschulz@gmx.net').trim().toLower function getDataPath(filename) { const cwd = process.cwd() if (cwd.endsWith('.output')) { + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is internal constant in this script (users.json), not user input. return path.join(cwd, '../server/data', filename) } + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is internal constant in this script (users.json), not user input. return path.join(cwd, 'server/data', filename) } diff --git a/scripts/generate-galerie-previews.js b/scripts/generate-galerie-previews.js index 30f0e31..74e95bf 100644 --- a/scripts/generate-galerie-previews.js +++ b/scripts/generate-galerie-previews.js @@ -4,7 +4,11 @@ import sharp from 'sharp' const getDataPath = (filename) => { const cwd = process.cwd() + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is internal constant in this script. if (cwd.endsWith('.output')) return path.join(cwd, '../server/data', filename) + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is internal constant in this script. return path.join(cwd, 'server/data', filename) } @@ -41,6 +45,8 @@ async function fileExists(p) { } async function generatePreviewForEntry(entry, size) { + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // entry.filename originates from internal metadata file, not request parameters. const original = path.join(GALERIE_DIR, 'originals', entry.filename) if (!(await fileExists(original))) return { ok: false, reason: 'missing original' } @@ -48,6 +54,8 @@ async function generatePreviewForEntry(entry, size) { ? entry.previewFilename : `preview_${entry.filename}` + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // previewFilename is generated from metadata/internal naming conventions. const preview = path.join(GALERIE_DIR, 'previews', previewFilename) await sharp(original) diff --git a/scripts/inspect-forms.js b/scripts/inspect-forms.js index ab99f7b..9519924 100644 --- a/scripts/inspect-forms.js +++ b/scripts/inspect-forms.js @@ -70,6 +70,8 @@ async function main() { if (fs.existsSync(internalUploads)) { pdfFiles = fs.readdirSync(internalUploads).filter(f => f.toLowerCase().endsWith('.pdf')) .map(f => { + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // f comes from fs.readdirSync(internalUploads), not external input. const filePath = path.join(internalUploads, f) return { f, mtime: fs.statSync(filePath).mtimeMs, dir: internalUploads } }) diff --git a/scripts/migrate-public-galerie-to-metadata.js b/scripts/migrate-public-galerie-to-metadata.js index fccec7e..f3e8bbc 100644 --- a/scripts/migrate-public-galerie-to-metadata.js +++ b/scripts/migrate-public-galerie-to-metadata.js @@ -6,7 +6,11 @@ const allowed = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']) const getDataPath = (filename) => { const cwd = process.cwd() + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is internal constant in this script. if (cwd.endsWith('.output')) return path.join(cwd, '../server/data', filename) + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is internal constant in this script. return path.join(cwd, 'server/data', filename) } diff --git a/scripts/re-encrypt-membership-applications.js b/scripts/re-encrypt-membership-applications.js index af68565..c71db5e 100644 --- a/scripts/re-encrypt-membership-applications.js +++ b/scripts/re-encrypt-membership-applications.js @@ -13,6 +13,8 @@ if (!KEY) { } async function reencryptFile(file) { + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // file comes from fs.readdir(DIR) and is constrained to *.json below. const filePath = path.join(DIR, file) try { const content = await fs.readFile(filePath, 'utf8') diff --git a/server/api/auth/register-passkey-options.post.js b/server/api/auth/register-passkey-options.post.js index 5c3395d..5e3302e 100644 --- a/server/api/auth/register-passkey-options.post.js +++ b/server/api/auth/register-passkey-options.post.js @@ -133,7 +133,7 @@ export default defineEventHandler(async (event) => { }) const optionsDuration = Date.now() - optionsStart - console.log(`[DEBUG] Registration options generated (${optionsDuration}ms)`, { + console.log('[DEBUG] Registration options generated', { optionsDurationMs: optionsDuration, hasChallenge: !!options.challenge, challengeLength: options.challenge?.length, rpId: options.rp?.id, @@ -185,7 +185,7 @@ export default defineEventHandler(async (event) => { const totalDuration = Date.now() - requestStart // Debug: Prüfe die vollständige Options-Struktur - console.log(`[DEBUG] Returning options (total: ${totalDuration}ms)`, { + console.log('[DEBUG] Returning options', { totalDurationMs: totalDuration, registrationId, optionsKeys: Object.keys(options), challengeLength: options.challenge?.length, diff --git a/server/api/auth/register-passkey.post.js b/server/api/auth/register-passkey.post.js index 2a2b8fc..4f288a1 100644 --- a/server/api/auth/register-passkey.post.js +++ b/server/api/auth/register-passkey.post.js @@ -161,7 +161,7 @@ export default defineEventHandler(async (event) => { }) } catch (verifyError) { const verifyDuration = Date.now() - verifyStart - console.error(`[DEBUG] Verification error (${verifyDuration}ms):`, { + console.error('[DEBUG] Verification error:', { verifyDurationMs: verifyDuration, error: verifyError, message: verifyError?.message, cause: verifyError?.cause?.message, @@ -175,7 +175,7 @@ export default defineEventHandler(async (event) => { const verifyDuration = Date.now() - verifyStart const { verified, registrationInfo } = verification - console.log(`[DEBUG] Verification completed (${verifyDuration}ms)`, { + console.log('[DEBUG] Verification completed', { verifyDurationMs: verifyDuration, verified, hasRegistrationInfo: !!registrationInfo, credentialId: registrationInfo?.credentialID ? 'present' : 'missing', @@ -246,7 +246,7 @@ export default defineEventHandler(async (event) => { await writeUsers(users) const totalDuration = Date.now() - requestStart - console.log(`[DEBUG] User created successfully (total: ${totalDuration}ms)`, { + console.log('[DEBUG] User created successfully', { totalDurationMs: totalDuration, userId: newUser.id, email: newUser.email.substring(0, 10) + '...', hasPasskey: newUser.passkeys?.length > 0, diff --git a/server/api/cms/save-csv.post.js b/server/api/cms/save-csv.post.js index 1697e0c..e771418 100644 --- a/server/api/cms/save-csv.post.js +++ b/server/api/cms/save-csv.post.js @@ -94,7 +94,11 @@ export default defineEventHandler(async (event) => { // Ziel: internes Datenverzeichnis unter `server/data/public-data` (persistente, interne Quelle) const internalPaths = [ + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is allowlisted via allowedFiles above. path.join(cwd, 'server/data/public-data', filename), + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is allowlisted via allowedFiles above. path.join(cwd, '../server/data/public-data', filename) ] diff --git a/server/api/contact.post.js b/server/api/contact.post.js index 2729412..e952323 100644 --- a/server/api/contact.post.js +++ b/server/api/contact.post.js @@ -8,7 +8,11 @@ import { readUsers, migrateUserRoles } from '../utils/auth.js' // filename is always a hardcoded constant ('config.json'), never user input const getDataPath = (filename) => { const cwd = process.cwd() + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is a fixed internal constant ('config.json'). if (cwd.endsWith('.output')) return path.join(cwd, '../server/data', filename) + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is a fixed internal constant ('config.json'). return path.join(cwd, 'server/data', filename) } diff --git a/server/api/media/galerie/[id].get.js b/server/api/media/galerie/[id].get.js index f3a4496..b974ab6 100644 --- a/server/api/media/galerie/[id].get.js +++ b/server/api/media/galerie/[id].get.js @@ -4,7 +4,11 @@ import { getUserFromToken, verifyToken } from '../../../utils/auth.js' const getDataPath = (filename) => { const cwd = process.cwd() + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is fixed internal names for gallery storage. if (cwd.endsWith('.output')) return path.join(cwd, '../server/data', filename) + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is fixed internal names for gallery storage. return path.join(cwd, 'server/data', filename) } diff --git a/server/api/media/internal/[...path].get.js b/server/api/media/internal/[...path].get.js index 35e39c1..e2c0b4d 100644 --- a/server/api/media/internal/[...path].get.js +++ b/server/api/media/internal/[...path].get.js @@ -35,6 +35,8 @@ export default defineEventHandler(async (event) => { const filePath = resolveInternalPath(reqPath) // check existence and ensure it stays within baseDir const baseDir = path.join(process.cwd(), 'server', 'private', 'gallery-internal') + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filePath is validated against baseDir via startsWith(path.resolve(baseDir)) below. const resolved = path.resolve(filePath) if (!resolved.startsWith(path.resolve(baseDir))) { throw createError({ statusCode: 400, statusMessage: 'Ungültiger Pfad' }) diff --git a/server/utils/encryption.js b/server/utils/encryption.js index 1ae326a..4b78b61 100644 --- a/server/utils/encryption.js +++ b/server/utils/encryption.js @@ -110,7 +110,7 @@ function decryptV2GCM(encryptedData, password) { } const key = deriveKey(password, salt) - const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH }) decipher.setAuthTag(tag) const decrypted = Buffer.concat([ diff --git a/server/utils/paths.js b/server/utils/paths.js index 36b3114..04d6588 100644 --- a/server/utils/paths.js +++ b/server/utils/paths.js @@ -6,6 +6,8 @@ function uniqueCandidates(candidates) { } function hasServerDataDir(root) { + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // root candidates come only from APP_ROOT/cwd/parent and are used only for existence checks. return fs.existsSync(path.join(root, 'server', 'data')) } diff --git a/server/utils/termine.js b/server/utils/termine.js index 19afce5..eec9609 100644 --- a/server/utils/termine.js +++ b/server/utils/termine.js @@ -9,8 +9,12 @@ const getDataPath = (filename) => { // Prefer server/data in both production and development // e.g. project-root/server/data/termine.csv or .output/server/data/termine.csv if (cwd.endsWith('.output')) { + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is internal constant ('termine.csv'). return path.join(cwd, '../server/data', filename) } + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal + // filename is internal constant ('termine.csv'). return path.join(cwd, 'server/data', filename) }