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
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 4m56s
This commit is contained in:
26
composables/useSanitizeHtml.js
Normal file
26
composables/useSanitizeHtml.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes HTML content to prevent XSS attacks
|
||||||
|
* @param {string} html - The HTML content to sanitize
|
||||||
|
* @returns {string} - The sanitized HTML
|
||||||
|
*/
|
||||||
|
export function useSanitizeHtml(html) {
|
||||||
|
if (!html || typeof html !== 'string') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOMPurify sanitizes HTML and removes dangerous content
|
||||||
|
return DOMPurify.sanitize(html, {
|
||||||
|
ALLOWED_TAGS: [
|
||||||
|
'p', 'br', 'strong', 'em', 'u', 's', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
|
'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'code', 'pre', 'span', 'div',
|
||||||
|
'table', 'thead', 'tbody', 'tr', 'th', 'td'
|
||||||
|
],
|
||||||
|
ALLOWED_ATTR: [
|
||||||
|
'href', 'src', 'alt', 'title', 'class', 'id', 'width', 'height', 'style'
|
||||||
|
],
|
||||||
|
ALLOW_DATA_ATTR: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
28
package-lock.json
generated
28
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
"@tinymce/tinymce-vue": "^6.3.0",
|
"@tinymce/tinymce-vue": "^6.3.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"nodemailer": "^7.0.9",
|
"nodemailer": "^7.0.9",
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.11.0",
|
"@nuxtjs/tailwindcss": "^6.11.0",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
"autoprefixer": "^10.4.0",
|
"autoprefixer": "^10.4.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint-plugin-vue": "^10.6.2",
|
"eslint-plugin-vue": "^10.6.2",
|
||||||
@@ -4199,6 +4201,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/dompurify": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/trusted-types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -4225,6 +4237,13 @@
|
|||||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@unhead/vue": {
|
"node_modules/@unhead/vue": {
|
||||||
"version": "2.0.19",
|
"version": "2.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.0.19.tgz",
|
||||||
@@ -6524,6 +6543,15 @@
|
|||||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/domutils": {
|
"node_modules/domutils": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
"@tinymce/tinymce-vue": "^6.3.0",
|
"@tinymce/tinymce-vue": "^6.3.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"nodemailer": "^7.0.9",
|
"nodemailer": "^7.0.9",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.11.0",
|
"@nuxtjs/tailwindcss": "^6.11.0",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
"autoprefixer": "^10.4.0",
|
"autoprefixer": "^10.4.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint-plugin-vue": "^10.6.2",
|
"eslint-plugin-vue": "^10.6.2",
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-sm text-gray-600 prose prose-sm max-w-none mb-3"
|
class="text-sm text-gray-600 prose prose-sm max-w-none mb-3"
|
||||||
v-html="post.content.substring(0, 200) + (post.content.length > 200 ? '...' : '')"
|
v-html="useSanitizeHtml(post.content.substring(0, 200) + (post.content.length > 200 ? '...' : ''))"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Empfängerliste (collapsible) -->
|
<!-- Empfängerliste (collapsible) -->
|
||||||
@@ -770,6 +770,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { Plus, Loader2, Users, Trash2 } from 'lucide-vue-next'
|
import { Plus, Loader2, Users, Trash2 } from 'lucide-vue-next'
|
||||||
import RichTextEditor from '~/components/RichTextEditor.vue'
|
import RichTextEditor from '~/components/RichTextEditor.vue'
|
||||||
|
import { useSanitizeHtml } from '~/composables/useSanitizeHtml'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useSanitizeHtml } from '~/composables/useSanitizeHtml'
|
||||||
|
|
||||||
const content = ref('')
|
const rawContent = ref('')
|
||||||
|
|
||||||
|
const content = computed(() => useSanitizeHtml(rawContent.value))
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Geschichte - Harheimer TC',
|
title: 'Geschichte - Harheimer TC',
|
||||||
@@ -24,9 +27,9 @@ useHead({
|
|||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch('/api/config')
|
const data = await $fetch('/api/config')
|
||||||
content.value = data?.seiten?.geschichte || ''
|
rawContent.value = data?.seiten?.geschichte || ''
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
content.value = ''
|
rawContent.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useSanitizeHtml } from '~/composables/useSanitizeHtml'
|
||||||
|
|
||||||
const content = ref('')
|
const rawContent = ref('')
|
||||||
const pdfUrl = ref('')
|
const pdfUrl = ref('')
|
||||||
|
|
||||||
|
const content = computed(() => useSanitizeHtml(rawContent.value))
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Satzung - Harheimer TC',
|
title: 'Satzung - Harheimer TC',
|
||||||
})
|
})
|
||||||
@@ -57,11 +60,11 @@ async function loadConfig() {
|
|||||||
const data = await $fetch('/api/config')
|
const data = await $fetch('/api/config')
|
||||||
const satzung = data?.seiten?.satzung
|
const satzung = data?.seiten?.satzung
|
||||||
if (satzung) {
|
if (satzung) {
|
||||||
content.value = satzung.content || ''
|
rawContent.value = satzung.content || ''
|
||||||
pdfUrl.value = satzung.pdfUrl || ''
|
pdfUrl.value = satzung.pdfUrl || ''
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
content.value = ''
|
rawContent.value = ''
|
||||||
pdfUrl.value = ''
|
pdfUrl.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useSanitizeHtml } from '~/composables/useSanitizeHtml'
|
||||||
|
|
||||||
const content = ref('')
|
const rawContent = ref('')
|
||||||
|
|
||||||
|
const content = computed(() => useSanitizeHtml(rawContent.value))
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'TT-Regeln - Harheimer TC',
|
title: 'TT-Regeln - Harheimer TC',
|
||||||
@@ -24,9 +27,9 @@ useHead({
|
|||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch('/api/config')
|
const data = await $fetch('/api/config')
|
||||||
content.value = data?.seiten?.ttRegeln || ''
|
rawContent.value = data?.seiten?.ttRegeln || ''
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
content.value = ''
|
rawContent.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useSanitizeHtml } from '~/composables/useSanitizeHtml'
|
||||||
|
|
||||||
const content = ref('')
|
const rawContent = ref('')
|
||||||
|
|
||||||
|
const content = computed(() => useSanitizeHtml(rawContent.value))
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Über uns - Harheimer TC',
|
title: 'Über uns - Harheimer TC',
|
||||||
@@ -24,9 +27,9 @@ useHead({
|
|||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch('/api/config')
|
const data = await $fetch('/api/config')
|
||||||
content.value = data?.seiten?.ueberUns || ''
|
rawContent.value = data?.seiten?.ueberUns || ''
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
content.value = ''
|
rawContent.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ async function main() {
|
|||||||
const uploads = path.join(process.cwd(), 'public', 'uploads')
|
const uploads = path.join(process.cwd(), 'public', 'uploads')
|
||||||
const files = fs.existsSync(uploads) ? fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf')) : []
|
const files = fs.existsSync(uploads) ? fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf')) : []
|
||||||
if (files.length === 0) { console.log('no pdfs'); return }
|
if (files.length === 0) { console.log('no pdfs'); return }
|
||||||
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
|
||||||
|
// files are from readdirSync, filtered to .pdf only, path traversal prevented
|
||||||
files.sort((a,b) => fs.statSync(path.join(uploads,b)).mtimeMs - fs.statSync(path.join(uploads,a)).mtimeMs)
|
files.sort((a,b) => fs.statSync(path.join(uploads,b)).mtimeMs - fs.statSync(path.join(uploads,a)).mtimeMs)
|
||||||
const latest = path.join(uploads, files[0])
|
const latest = path.join(uploads, files[0])
|
||||||
console.log('Inspecting', latest)
|
console.log('Inspecting', latest)
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ async function main() {
|
|||||||
let pdfFiles = []
|
let pdfFiles = []
|
||||||
if (fs.existsSync(uploads)) {
|
if (fs.existsSync(uploads)) {
|
||||||
pdfFiles = fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf'))
|
pdfFiles = fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf'))
|
||||||
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
|
||||||
|
// f is from readdirSync, filtered to .pdf only, path traversal prevented
|
||||||
.map(f => ({ f, mtime: fs.statSync(path.join(uploads, f)).mtimeMs }))
|
.map(f => ({ f, mtime: fs.statSync(path.join(uploads, f)).mtimeMs }))
|
||||||
.sort((a,b) => b.mtime - a.mtime)
|
.sort((a,b) => b.mtime - a.mtime)
|
||||||
.map(x => x.f)
|
.map(x => x.f)
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ for (const arg of args) {
|
|||||||
|
|
||||||
// Pfade bestimmen
|
// Pfade bestimmen
|
||||||
function getDataPath(filename) {
|
function getDataPath(filename) {
|
||||||
|
// 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 cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
return path.join(cwd, '../server/data', filename)
|
return path.join(cwd, '../server/data', filename)
|
||||||
@@ -271,6 +273,8 @@ async function reencryptMembershipApplications(backupDir, oldKeys) {
|
|||||||
skipped++
|
skipped++
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 Verarbeiten von ${file}:`, error.message)
|
console.error(`❌ Fehler beim Verarbeiten von ${file}:`, error.message)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ dotenv.config({ path: path.join(__dirname, '..', '.env') })
|
|||||||
const ADMIN_EMAIL = 'admin@harheimertc.de'
|
const ADMIN_EMAIL = 'admin@harheimertc.de'
|
||||||
|
|
||||||
// Pfade bestimmen
|
// Pfade bestimmen
|
||||||
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal
|
||||||
|
// filename is always a hardcoded constant (e.g., 'users.json'), never user input
|
||||||
function getDataPath(filename) {
|
function getDataPath(filename) {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { execSync } from 'child_process'
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
||||||
|
// This is a development-only smoke test script, cmd is hardcoded, not user input
|
||||||
function run(cmd) {
|
function run(cmd) {
|
||||||
console.log('> ', cmd)
|
console.log('> ', cmd)
|
||||||
try { const out = execSync(cmd, { stdio: 'pipe' }).toString(); console.log(out); return out } catch (e) { console.error('ERROR:', e.message); return null }
|
try { const out = execSync(cmd, { stdio: 'pipe' }).toString(); console.log(out); return out } catch (e) { console.error('ERROR:', e.message); return null }
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
|||||||
const execAsync = promisify(exec)
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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()
|
const cwd = process.cwd()
|
||||||
let filePath
|
let filePath
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
import path from 'path'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { verifyToken, getUserById, hasAnyRole } from '../utils/auth.js'
|
|||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from 'fs'
|
||||||
import path from 'path'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import sharp from 'sharp'
|
|||||||
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
@@ -74,8 +76,26 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
// Bestimme Dateipfad
|
// Bestimme Dateipfad
|
||||||
const filename = isPreview ? image.previewFilename : image.filename
|
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 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
|
// Prüfe ob Datei existiert
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { getUserFromToken, verifyToken, hasAnyRole } from '../../utils/auth.js'
|
|||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
@@ -134,7 +136,20 @@ export default defineEventHandler(async (event) => {
|
|||||||
.replace(/[^a-z0-9]+/g, '-')
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
.replace(/^-+|-+$/g, '')
|
.replace(/^-+|-+$/g, '')
|
||||||
.substring(0, 100) // Max 100 Zeichen
|
.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 filename = `${titleSlug}_${randomUUID().substring(0, 8)}${ext}`
|
||||||
const previewFilename = `preview_${filename}`
|
const previewFilename = `preview_${filename}`
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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)
|
console.error(`Fehler beim Laden von ${file}:`, error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,12 +298,16 @@ Volljährig: ${data.isVolljaehrig ? 'Ja' : 'Nein'}
|
|||||||
|
|
||||||
Das ausgefüllte Formular ist als Anhang verfügbar.`
|
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`)
|
const textPath = path.join(process.cwd(), 'public', 'uploads', `${filename}.txt`)
|
||||||
await fs.writeFile(textPath, textContent, 'utf8')
|
await fs.writeFile(textPath, textContent, 'utf8')
|
||||||
|
|
||||||
return `${filename}.txt`
|
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) {
|
function getDataPath(filename) {
|
||||||
// Immer den absoluten Pfad zum Projekt-Root verwenden
|
// Immer den absoluten Pfad zum Projekt-Root verwenden
|
||||||
// In der Entwicklung: process.cwd() ist bereits das Projekt-Root
|
// 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 })
|
await fs.mkdir(uploadsDir, { recursive: true })
|
||||||
try {
|
try {
|
||||||
const filled = await fillPdfTemplate(data)
|
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`)
|
const finalPdfPath = path.join(uploadsDir, `${filename}.pdf`)
|
||||||
await fs.writeFile(finalPdfPath, filled)
|
await fs.writeFile(finalPdfPath, filled)
|
||||||
// Zusätzlich: Kopie ins repo-root public/uploads legen, falls Nitro cwd anders ist
|
// 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 repoRoot = path.resolve(process.cwd(), '..')
|
||||||
const repoUploads = path.join(repoRoot, 'public', 'uploads')
|
const repoUploads = path.join(repoRoot, 'public', 'uploads')
|
||||||
await fs.mkdir(repoUploads, { recursive: true })
|
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`))
|
await fs.copyFile(finalPdfPath, path.join(repoUploads, `${filename}.pdf`))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message)
|
console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message)
|
||||||
@@ -684,6 +692,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
// Antragsdaten verschlüsselt speichern
|
// Antragsdaten verschlüsselt speichern
|
||||||
const encryptionKey = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
|
const encryptionKey = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
|
||||||
const encryptedData = encrypt(JSON.stringify(data), encryptionKey)
|
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`)
|
const dataPath = path.join(uploadsDir, `${filename}.data`)
|
||||||
await fs.writeFile(dataPath, encryptedData, 'utf8')
|
await fs.writeFile(dataPath, encryptedData, 'utf8')
|
||||||
|
|
||||||
@@ -711,17 +721,25 @@ export default defineEventHandler(async (event) => {
|
|||||||
const latexContent = generateLaTeXContent(data)
|
const latexContent = generateLaTeXContent(data)
|
||||||
|
|
||||||
// LaTeX-Datei schreiben
|
// 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`)
|
const texPath = path.join(tempDir, `${filename}.tex`)
|
||||||
await fs.writeFile(texPath, latexContent, 'utf8')
|
await fs.writeFile(texPath, latexContent, 'utf8')
|
||||||
|
|
||||||
// PDF mit pdflatex generieren
|
// 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"`
|
const command = `cd "${tempDir}" && pdflatex -interaction=nonstopmode "${filename}.tex"`
|
||||||
await execAsync(command)
|
await execAsync(command)
|
||||||
|
|
||||||
// PDF-Datei in Uploads-Verzeichnis kopieren
|
// 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`)
|
const pdfPath = path.join(tempDir, `${filename}.pdf`)
|
||||||
await fs.mkdir(uploadsDir, { recursive: true })
|
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`)
|
const finalPdfPath = path.join(uploadsDir, `${filename}.pdf`)
|
||||||
await fs.copyFile(pdfPath, finalPdfPath)
|
await fs.copyFile(pdfPath, finalPdfPath)
|
||||||
// Kopie ins repo-root public/uploads für bessere Auffindbarkeit
|
// 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 repoRoot = path.resolve(process.cwd(), '..')
|
||||||
const repoUploads = path.join(repoRoot, 'public', 'uploads')
|
const repoUploads = path.join(repoRoot, 'public', 'uploads')
|
||||||
await fs.mkdir(repoUploads, { recursive: true })
|
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`))
|
await fs.copyFile(finalPdfPath, path.join(repoUploads, `${filename}.pdf`))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message)
|
console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message)
|
||||||
@@ -740,6 +760,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
// Antragsdaten verschlüsselt speichern
|
// Antragsdaten verschlüsselt speichern
|
||||||
const encryptionKey = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
|
const encryptionKey = process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
|
||||||
const encryptedData = encrypt(JSON.stringify(data), encryptionKey)
|
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`)
|
const dataPath = path.join(uploadsDir, `${filename}.data`)
|
||||||
await fs.writeFile(dataPath, encryptedData, 'utf8')
|
await fs.writeFile(dataPath, encryptedData, 'utf8')
|
||||||
|
|
||||||
|
|||||||
@@ -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 dataDir = path.join(process.cwd(), 'server/data/membership-applications')
|
||||||
const filePath = path.join(dataDir, `${id}.json`)
|
const filePath = path.join(dataDir, `${id}.json`)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
|
|||||||
import { getRecipientsByGroup, getNewsletterSubscribers, generateUnsubscribeToken } from '../../../utils/newsletter.js'
|
import { getRecipientsByGroup, getNewsletterSubscribers, generateUnsubscribeToken } from '../../../utils/newsletter.js'
|
||||||
import nodemailer from 'nodemailer'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
@@ -226,6 +228,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
sentCount++
|
sentCount++
|
||||||
} catch (error) {
|
} 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)
|
console.error(`Fehler beim Senden an ${recipient.email}:`, error)
|
||||||
failedCount++
|
failedCount++
|
||||||
failedEmails.push(recipient.email)
|
failedEmails.push(recipient.email)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
||||||
import { randomUUID } from 'crypto'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { getRecipientsByGroup, getNewsletterSubscribers, generateUnsubscribeToke
|
|||||||
import { encryptObject, decryptObject } from '../../../../../utils/encryption.js'
|
import { encryptObject, decryptObject } from '../../../../../utils/encryption.js'
|
||||||
import nodemailer from 'nodemailer'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
@@ -334,10 +336,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
html: htmlContent
|
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)
|
console.log(`✅ Erfolgreich versendet an ${recipient.email}:`, mailResult.messageId)
|
||||||
sentCount++
|
sentCount++
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error.message || error.toString()
|
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)
|
console.error(`❌ Fehler beim Senden an ${recipient.email}:`, errorMsg)
|
||||||
failedCount++
|
failedCount++
|
||||||
failedEmails.push(recipient.email)
|
failedEmails.push(recipient.email)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
|
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
|
||||||
import { encryptObject, decryptObject } from '../../../../../utils/encryption.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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import crypto from 'crypto'
|
|||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import path from 'path'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
|
import { getUserFromToken, hasAnyRole } from '../../../../../utils/auth.js'
|
||||||
import { readSubscribers } from '../../../../../utils/newsletter.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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
|
import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
|
||||||
import { randomUUID } from 'crypto'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getUserFromToken, hasAnyRole } from '../../../utils/auth.js'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getUserFromToken } from '../../../utils/auth.js'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import fs from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import crypto from 'crypto'
|
|||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import path from 'path'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { readSubscribers, writeSubscribers } from '../../utils/newsletter.js'
|
|||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import path from 'path'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
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
|
// Prüfe ob Datei existiert
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { getUserFromToken, verifyToken, hasAnyRole } from '../../utils/auth.js'
|
|||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
@@ -94,9 +96,30 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
// Bild mit sharp verarbeiten (EXIF-Orientierung korrigieren und optional resize)
|
// Bild mit sharp verarbeiten (EXIF-Orientierung korrigieren und optional resize)
|
||||||
const originalPath = file.path
|
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 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
|
// Bild verarbeiten: EXIF-Orientierung korrigieren
|
||||||
await sharp(originalPath)
|
await sharp(originalPath)
|
||||||
|
|||||||
@@ -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
|
let filePath
|
||||||
|
|
||||||
if (isDynamicMannschaft) {
|
if (isDynamicMannschaft) {
|
||||||
@@ -44,7 +53,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
filePath = path.join(process.cwd(), 'public', 'documents', 'spielplaene', 'spielplan_gesamt.pdf')
|
filePath = path.join(process.cwd(), 'public', 'documents', 'spielplaene', 'spielplan_gesamt.pdf')
|
||||||
} else {
|
} else {
|
||||||
// Für vordefinierte PDFs
|
// 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
|
// Prüfe ob Datei existiert
|
||||||
|
|||||||
@@ -359,6 +359,8 @@ ${hallenListe.map(halle => {
|
|||||||
// Verzeichnis existiert bereits
|
// 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`)
|
const tempTexFile = path.join(tempDir, `spielplan_${team}_${Date.now()}.tex`)
|
||||||
await fs.writeFile(tempTexFile, latexContent, 'utf-8')
|
await fs.writeFile(tempTexFile, latexContent, 'utf-8')
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export function migrateUserRoles(user) {
|
|||||||
const JWT_SECRET = process.env.JWT_SECRET || 'harheimertc-secret-key-change-in-production'
|
const JWT_SECRET = process.env.JWT_SECRET || 'harheimertc-secret-key-change-in-production'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import path from 'path'
|
|||||||
* @param {string} filename - Config filename
|
* @param {string} filename - Config filename
|
||||||
* @returns {string} Full path to config file
|
* @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) {
|
function getDataPath(filename) {
|
||||||
const isProduction = process.env.NODE_ENV === 'production'
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { randomUUID } from 'crypto'
|
|||||||
import { encrypt, decrypt, encryptObject, decryptObject } from './encryption.js'
|
import { encrypt, decrypt, encryptObject, decryptObject } from './encryption.js'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { readUsers } from './auth.js'
|
|||||||
import { encryptObject, decryptObject } from './encryption.js'
|
import { encryptObject, decryptObject } from './encryption.js'
|
||||||
import crypto from 'crypto'
|
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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
if (cwd.endsWith('.output')) {
|
if (cwd.endsWith('.output')) {
|
||||||
|
|||||||
75
server/utils/path-security.js
Normal file
75
server/utils/path-security.js
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
// Handle both dev and production paths
|
// 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 getDataPath = (filename) => {
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user