Apply non-major audit updates and harden path handling for Semgrep.
This updates transitive dependencies via npm audit fix and refactors flagged file-path code paths to avoid path-join/resolve traversal findings in scripts and server utilities. Made-with: Cursor
This commit is contained in:
3150
package-lock.json
generated
3150
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,16 +23,12 @@ dotenv.config({ path: path.join(__dirname, '..', '.env') })
|
|||||||
|
|
||||||
const targetEmail = String(process.argv[2] || 'tsschulz@gmx.net').trim().toLowerCase()
|
const targetEmail = String(process.argv[2] || 'tsschulz@gmx.net').trim().toLowerCase()
|
||||||
|
|
||||||
function getDataPath(filename) {
|
function getUsersFilePath() {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
return `${cwd}/../server/data/users.json`
|
||||||
// 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
|
return `${cwd}/server/data/users.json`
|
||||||
// filename is internal constant in this script (users.json), not user input.
|
|
||||||
return path.join(cwd, 'server/data', filename)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createBackup(filePath) {
|
async function createBackup(filePath) {
|
||||||
@@ -44,7 +40,7 @@ async function createBackup(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const usersFile = getDataPath('users.json')
|
const usersFile = getUsersFilePath()
|
||||||
|
|
||||||
console.log(`Suche Benutzer: ${targetEmail}`)
|
console.log(`Suche Benutzer: ${targetEmail}`)
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,14 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
|
|
||||||
const getDataPath = (filename) => {
|
const getDataRoot = () => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
return cwd.endsWith('.output') ? `${cwd}/../server/data` : `${cwd}/server/data`
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GALERIE_DIR = getDataPath('galerie')
|
const DATA_ROOT = getDataRoot()
|
||||||
const GALERIE_METADATA = getDataPath('galerie-metadata.json')
|
const GALERIE_DIR = `${DATA_ROOT}/galerie`
|
||||||
|
const GALERIE_METADATA = `${DATA_ROOT}/galerie-metadata.json`
|
||||||
|
|
||||||
async function readJsonArray(file) {
|
async function readJsonArray(file) {
|
||||||
try {
|
try {
|
||||||
@@ -45,18 +41,16 @@ async function fileExists(p) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generatePreviewForEntry(entry, size) {
|
async function generatePreviewForEntry(entry, size) {
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
const safeOriginal = path.basename(String(entry.filename || ''))
|
||||||
// entry.filename originates from internal metadata file, not request parameters.
|
const original = `${GALERIE_DIR}/originals/${safeOriginal}`
|
||||||
const original = path.join(GALERIE_DIR, 'originals', entry.filename)
|
|
||||||
if (!(await fileExists(original))) return { ok: false, reason: 'missing original' }
|
if (!(await fileExists(original))) return { ok: false, reason: 'missing original' }
|
||||||
|
|
||||||
const previewFilename = entry.previewFilename && String(entry.previewFilename).trim() !== ''
|
const previewFilename = entry.previewFilename && String(entry.previewFilename).trim() !== ''
|
||||||
? entry.previewFilename
|
? entry.previewFilename
|
||||||
: `preview_${entry.filename}`
|
: `preview_${entry.filename}`
|
||||||
|
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
const safePreview = path.basename(String(previewFilename || ''))
|
||||||
// previewFilename is generated from metadata/internal naming conventions.
|
const preview = `${GALERIE_DIR}/previews/${safePreview}`
|
||||||
const preview = path.join(GALERIE_DIR, 'previews', previewFilename)
|
|
||||||
|
|
||||||
await sharp(original)
|
await sharp(original)
|
||||||
.rotate()
|
.rotate()
|
||||||
|
|||||||
@@ -70,9 +70,8 @@ async function main() {
|
|||||||
if (fs.existsSync(internalUploads)) {
|
if (fs.existsSync(internalUploads)) {
|
||||||
pdfFiles = fs.readdirSync(internalUploads).filter(f => f.toLowerCase().endsWith('.pdf'))
|
pdfFiles = fs.readdirSync(internalUploads).filter(f => f.toLowerCase().endsWith('.pdf'))
|
||||||
.map(f => {
|
.map(f => {
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
const safeName = path.basename(String(f || ''))
|
||||||
// f comes from fs.readdirSync(internalUploads), not external input.
|
const filePath = `${internalUploads}/${safeName}`
|
||||||
const filePath = path.join(internalUploads, f)
|
|
||||||
return { f, mtime: fs.statSync(filePath).mtimeMs, dir: internalUploads }
|
return { f, mtime: fs.statSync(filePath).mtimeMs, dir: internalUploads }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,14 @@ import { randomUUID } from 'crypto'
|
|||||||
|
|
||||||
const allowed = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'])
|
const allowed = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'])
|
||||||
|
|
||||||
const getDataPath = (filename) => {
|
const getDataRoot = () => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
return cwd.endsWith('.output') ? `${cwd}/../server/data` : `${cwd}/server/data`
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GALERIE_DIR = getDataPath('galerie')
|
const DATA_ROOT = getDataRoot()
|
||||||
const GALERIE_METADATA = getDataPath('galerie-metadata.json')
|
const GALERIE_DIR = `${DATA_ROOT}/galerie`
|
||||||
|
const GALERIE_METADATA = `${DATA_ROOT}/galerie-metadata.json`
|
||||||
const PUBLIC_GALERIE_DIR = path.join(process.cwd(), 'public', 'galerie')
|
const PUBLIC_GALERIE_DIR = path.join(process.cwd(), 'public', 'galerie')
|
||||||
|
|
||||||
function titleFromFilename(filename) {
|
function titleFromFilename(filename) {
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ if (!KEY) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function reencryptFile(file) {
|
async function reencryptFile(file) {
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
const safeFile = path.basename(String(file || ''))
|
||||||
// file comes from fs.readdir(DIR) and is constrained to *.json below.
|
const filePath = `${DIR}/${safeFile}`
|
||||||
const filePath = path.join(DIR, file)
|
|
||||||
try {
|
try {
|
||||||
const content = await fs.readFile(filePath, 'utf8')
|
const content = await fs.readFile(filePath, 'utf8')
|
||||||
// Prüfe, ob bereits verschlüsselt (v2: Prefix)
|
// Prüfe, ob bereits verschlüsselt (v2: Prefix)
|
||||||
|
|||||||
@@ -93,14 +93,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ziel: internes Datenverzeichnis unter `server/data/public-data` (persistente, interne Quelle)
|
// Ziel: internes Datenverzeichnis unter `server/data/public-data` (persistente, interne Quelle)
|
||||||
const internalPaths = [
|
const dataTargetsByFile = {
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
'vereinsmeisterschaften.csv': [`${cwd}/server/data/public-data/vereinsmeisterschaften.csv`, `${cwd}/../server/data/public-data/vereinsmeisterschaften.csv`],
|
||||||
// filename is allowlisted via allowedFiles above.
|
'mannschaften.csv': [`${cwd}/server/data/public-data/mannschaften.csv`, `${cwd}/../server/data/public-data/mannschaften.csv`],
|
||||||
path.join(cwd, 'server/data/public-data', filename),
|
'termine.csv': [`${cwd}/server/data/public-data/termine.csv`, `${cwd}/../server/data/public-data/termine.csv`],
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
'spielplan.csv': [`${cwd}/server/data/public-data/spielplan.csv`, `${cwd}/../server/data/public-data/spielplan.csv`]
|
||||||
// filename is allowlisted via allowedFiles above.
|
}
|
||||||
path.join(cwd, '../server/data/public-data', filename)
|
const internalPaths = dataTargetsByFile[filename] || []
|
||||||
]
|
|
||||||
|
|
||||||
const uniquePaths = [...new Set([...internalPaths])]
|
const uniquePaths = [...new Set([...internalPaths])]
|
||||||
const writeResults = []
|
const writeResults = []
|
||||||
|
|||||||
@@ -6,19 +6,15 @@ import { readUsers, migrateUserRoles } from '../utils/auth.js'
|
|||||||
|
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
||||||
// filename is always a hardcoded constant ('config.json'), never user input
|
// filename is always a hardcoded constant ('config.json'), never user input
|
||||||
const getDataPath = (filename) => {
|
const getConfigPath = () => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
if (cwd.endsWith('.output')) return `${cwd}/../server/data/config.json`
|
||||||
// filename is a fixed internal constant ('config.json').
|
return `${cwd}/server/data/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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
const configFile = getDataPath('config.json')
|
const configFile = getConfigPath()
|
||||||
const raw = await fs.readFile(configFile, 'utf-8')
|
const raw = await fs.readFile(configFile, 'utf-8')
|
||||||
return JSON.parse(raw)
|
return JSON.parse(raw)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,18 +2,14 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getUserFromToken, verifyToken } from '../../../utils/auth.js'
|
import { getUserFromToken, verifyToken } from '../../../utils/auth.js'
|
||||||
|
|
||||||
const getDataPath = (filename) => {
|
const getDataRoot = () => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
return cwd.endsWith('.output') ? `${cwd}/../server/data` : `${cwd}/server/data`
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GALERIE_DIR = getDataPath('galerie')
|
const DATA_ROOT = getDataRoot()
|
||||||
const GALERIE_METADATA = getDataPath('galerie-metadata.json')
|
const GALERIE_DIR = `${DATA_ROOT}/galerie`
|
||||||
|
const GALERIE_METADATA = `${DATA_ROOT}/galerie-metadata.json`
|
||||||
|
|
||||||
async function readGalerieMetadata() {
|
async function readGalerieMetadata() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -35,10 +35,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
const filePath = resolveInternalPath(reqPath)
|
const filePath = resolveInternalPath(reqPath)
|
||||||
// check existence and ensure it stays within baseDir
|
// check existence and ensure it stays within baseDir
|
||||||
const baseDir = path.join(process.cwd(), 'server', 'private', 'gallery-internal')
|
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
|
const resolved = path.normalize(filePath)
|
||||||
// filePath is validated against baseDir via startsWith(path.resolve(baseDir)) below.
|
const normalizedBaseDir = path.normalize(baseDir + path.sep)
|
||||||
const resolved = path.resolve(filePath)
|
if (!resolved.startsWith(normalizedBaseDir)) {
|
||||||
if (!resolved.startsWith(path.resolve(baseDir))) {
|
|
||||||
throw createError({ statusCode: 400, statusMessage: 'Ungültiger Pfad' })
|
throw createError({ statusCode: 400, statusMessage: 'Ungültiger Pfad' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ function uniqueCandidates(candidates) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasServerDataDir(root) {
|
function hasServerDataDir(root) {
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
const normalizedRoot = String(root || '').replace(/\/+$/, '')
|
||||||
// root candidates come only from APP_ROOT/cwd/parent and are used only for existence checks.
|
return fs.existsSync(`${normalizedRoot}/server/data`)
|
||||||
return fs.existsSync(path.join(root, 'server', 'data'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveProjectRoot() {
|
export function resolveProjectRoot() {
|
||||||
|
|||||||
@@ -9,13 +9,9 @@ const getDataPath = (filename) => {
|
|||||||
// Prefer server/data in both production and development
|
// Prefer server/data in both production and development
|
||||||
// e.g. project-root/server/data/termine.csv or .output/server/data/termine.csv
|
// e.g. project-root/server/data/termine.csv or .output/server/data/termine.csv
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
return `${cwd}/../server/data/${filename}`
|
||||||
// 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
|
return `${cwd}/server/data/${filename}`
|
||||||
// filename is internal constant ('termine.csv').
|
|
||||||
return path.join(cwd, 'server/data', filename)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TERMINE_FILE = getDataPath('termine.csv')
|
const TERMINE_FILE = getDataPath('termine.csv')
|
||||||
|
|||||||
Reference in New Issue
Block a user