Fix installer pack fetch and simplify step 1 UI

The installer pulls each pack JSON from /manifest/<file>.json, but the
server was blocking every /manifest/ path. Add a strict whitelist
route that only serves /manifest/<a-zA-Z0-9_-name>.json files (no
directory listing, no path traversal, no other extensions); keep the
catch-all 404 for anything else under /manifest/.

Also strip the manifest URL input and "목록 새로고침" button from
installer step 1 — packs auto-load on page render, only the selectable
list remains.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 21:44:50 +09:00
parent 8fd7cfaaef
commit bda79e18eb
2 changed files with 37 additions and 22 deletions

View File

@@ -1,7 +1,7 @@
import express from 'express'
import session from 'express-session'
import path from 'node:path'
import { manifestRootPath, fileDirPath, viewsDirPath, publicDirPath } from '../shared/paths'
import { manifestRootPath, manifestDirPath, fileDirPath, viewsDirPath, publicDirPath } from '../shared/paths'
import { indexRouter } from './routes/index'
import { opRouter } from './routes/op'
@@ -28,9 +28,9 @@ app.use(session({
}
}))
// 외부에서 account.json, /manifest 폴더 등에 절대 접근 불가하도록 가장 먼저 차단한다.
// account.json은 외부 노출 절대 금지.
app.use((req, res, next) => {
if (/^\/account\.json/i.test(req.path) || /^\/manifest\//i.test(req.path)) {
if (/^\/account\.json/i.test(req.path)) {
res.status(404).send('Not Found')
return
}
@@ -44,6 +44,29 @@ app.get('/manifest.json', (_req, res) => {
res.sendFile(manifestRootPath)
})
// 설치기에서 개별 음악퀴즈 JSON을 가져갈 수 있도록 파일 단위로만 허용.
// 디렉토리 리스팅, 다른 확장자, 경로 탈출은 차단.
app.get('/manifest/:fileName', (req, res) => {
const fileName = req.params.fileName
if (!/^[a-zA-Z0-9_\-]+\.json$/.test(fileName)) {
res.status(404).send('Not Found')
return
}
res.sendFile(path.join(manifestDirPath, fileName), (err) => {
if (!err || res.headersSent) return
res.status(404).send('Not Found')
})
})
// 그 외 /manifest/ 하위 모든 요청 차단 (디렉토리 인덱스 포함).
app.use((req, res, next) => {
if (/^\/manifest\//i.test(req.path)) {
res.status(404).send('Not Found')
return
}
next()
})
app.use('/file', express.static(fileDirPath, { fallthrough: true, index: false }))
app.use('/', indexRouter)