์ปจํ
์คํธ ๋ธ๋ก
| Key |
Value |
| Category |
api |
| Checklist |
ISS-API-06 โ Admin API routes missing role-check middleware |
| Priority |
P0 ๐ด |
| Scan Date |
2026-04-16 |
| Flagged By |
@api-explorer |
์์ฝ
- WHAT:
GET /api/admin/upload/[filename] ๋ผ์ฐํธ์ ์ธ์ฆ ๊ฒ์ฌ๊ฐ ์ ํ ์์ด, ํ์ผ๋ช
๋ง ์๋ฉด ๋๊ตฌ๋(๋ก๊ทธ์ธ ์์ด๋) ์
๋ก๋๋ admin ๋ธ๋๋ฉ/๋ก๊ณ ์์ฐ์ ์ง์ ๋ค์ด๋ก๋ ๊ฐ๋ฅ
- WHY: ๋ค๋ฅธ ๋ชจ๋
/api/admin/** ํธ๋ค๋ฌ๋ auth() + isAdmin() ๊ฐ๋๋ฅผ ๊ฑฐ์น์ง๋ง ์ด ํ์ผ ์๋ธ ๋ผ์ฐํธ๋ง ๋ฌด๋ฐฉ๋น โ ๊ด๋ฆฌ์์ฉ ๋น๊ณต๊ฐ ํ์ผ(๋ก๊ณ ์ด์, ๋ด๋ถ ์ด๋ฏธ์ง)์ด ์ธํฐ๋ท์ ๋
ธ์ถ๋จ
- WHERE:
frontend/src/app/api/admin/upload/[filename]/route.ts:24 โ GET ํธ๋ค๋ฌ ๋ณธ๋ฌธ
- SEVERITY: CRITICAL โ ์ธ์ฆ ์์ ๋ถ์ฌ + admin ๋๋ฉ์ธ ๋ฆฌ์์ค ๊ณต๊ฐ ์ ๊ทผ
Evidence
| # |
File |
Line |
Finding |
Flagged By |
Confidence |
| 1 |
frontend/src/app/api/admin/upload/[filename]/route.ts |
24 |
GET ํธ๋ค๋ฌ์ auth() ํธ์ถ ์์, ํ์ผ ์์คํ
์์ ์ฆ์ ์ฝ์ด์ ๋ฐํ |
@api-explorer |
High |
| 2 |
frontend/src/app/api/admin/upload/route.ts |
35 |
POST(์
๋ก๋)๋ isAdmin ์ฒดํฌ ์กด์ฌ โ ์
๋ก๋๋ ๋ณดํธ, ๋ค์ด๋ก๋๋ ๋ฏธ๋ณดํธ (๋น๋์นญ) |
@api-explorer |
High |
์ํฅ ๋ถ์
์ํฅ ๋ฒ์
- ๋ชจ๋
/api/admin/upload/* ๊ฒฝ๋ก๋ก ์ ์ฅ๋ ์์ฐ (๋ก๊ณ , ๋ธ๋๋ฉ ์ด๋ฏธ์ง, admin-์ ์ฉ ์ํธ์ํฌ)
- ํ์์คํฌํ ๊ธฐ๋ฐ ํ์ผ๋ช
์ ์ถ์ธก ๊ฐ๋ฅ (
{timestamp}-{original}.ext ํจํด)
- ๋ธ๋ฃจํธํฌ์ค ๋๋ ์ธ๋ถ ๋ ํผ๋ฌ ๋
ธ์ถ(๋ธ๋ผ์ฐ์ ์บ์, ๋ก๊ทธ ํ์ผ)๋ก ํ์ผ๋ช
์ด ์ ์ถ๋๋ฉด ์ฆ์ ์ธ๋ถ ์ ๊ทผ ๊ฐ๋ฅ
์ฅ์ ์๋๋ฆฌ์ค
- ๊ด๋ฆฌ์๊ฐ admin ์ฝ์์์ ๋ฏธ๊ณต๊ฐ ๋ก๊ณ ์์์ ์
๋ก๋ (
POST /api/admin/upload โ ํ์ผ๋ช
๋ฐํ)
- ๋ฐํ๋ ํ์ผ๋ช
์ด ์ด๋ค ๊ฒฝ๋ก(๋ธ๋ผ์ฐ์ ํ์คํ ๋ฆฌ, ์๋ฒ ๋ก๊ทธ, CDN ์บ์)๋ก๋ ์ธ๋ถ์ ์ ์ถ
- ๋น๋ก๊ทธ์ธ ๊ณต๊ฒฉ์๊ฐ
GET /api/admin/upload/{filename}๋ก ๋ฐ๋ก ๋ค์ด๋ก๋ ์ฑ๊ณต
- ๋น๊ณต๊ฐ ์์ฐ ์ ์ถ, ๊ฐ์ฌ ๋ก๊ทธ ๋ฏธ๊ธฐ๋ก โ ํ์ง ์ด๋ ค์
๊ธด๊ธ๋
์ ์ ํด๊ฒฐ ๋ฐฉ์
์ ๊ทผ ๋ฐฉ๋ฒ
frontend/src/app/api/admin/upload/[filename]/route.ts:24 GET ํธ๋ค๋ฌ ์ ๋์ admin ๊ฐ๋ ์ฝ์
:
import { auth } from \"@/lib/auth\";
import { isAdmin } from \"@/lib/auth/is-admin\";
export async function GET(req: NextRequest, { params }: { params: { filename: string } }) {
const session = await auth();
if (!session?.user || !isAdmin(session.user)) {
return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });
}
// ... existing file serve logic
}
์ถ๊ฐ๋ก filename์ ๋ํ path-traversal ๋ฐฉ์ด (์ด๋ฏธ sanitize ๋์ด ์๋์ง ๊ฒ์ฆ โ .. / ์ ๋๊ฒฝ๋ก ๊ฑฐ๋ถ).
๋์
- ๊ณต๊ฐ ํ์ฉ + obfuscated ํ์ผ๋ช
: UUID v4 ๊ธฐ๋ฐ ๋๋ค ํ์ผ๋ช
์ผ๋ก ์ถ์ธก ๋ถ๊ฐ๋ฅํ๊ฒ ์ค๊ณ โ ๋ณด์ by obscurity๋ก ์ํฐํจํด, ์ธ์ฆ ์์์ ์ ๋นํํ์ง ์์
- Next.js middleware์์ ์ผ๊ด ์ฒ๋ฆฌ:
/api/admin/* ์ ์ฒด ๋งค์ฒ๋ก middleware์์ ์ธ์ฆ ๊ฐ์ โ ๊ฐ๋ฅํ์ง๋ง ๊ธฐ์กด ํธ๋ค๋ฌ๋ค์ด ์ด๋ฏธ ํธ๋ค๋ฌ ๋ ๋ฒจ์์ ๊ฐ๋ํ๋ฏ๋ก ์ผ๊ด์ฑ ์ํด ํธ๋ค๋ฌ ๋ ๋ฒจ ์ ์ง ๊ถ์ฅ
์์ฉ ๊ธฐ์ค
์ฐธ์กฐ
์ฌํ ๋ฐฉ๋ฒ
์ฌ์ ์กฐ๊ฑด
- dev ์๋ฒ ์คํ ์ค (
cd frontend && pnpm dev)
- admin ์ฝ์์์ ํ์ผ 1๊ฐ ์
๋ก๋ (๋ฐํ๋ filename ๊ธฐ๋ก)
๋จ๊ณ
- ๋ธ๋ผ์ฐ์ private ์ฐฝ ์ด๊ธฐ (์ธ์ฆ ์ฟ ํค ์ ๊ฑฐ)
http://localhost:3000/api/admin/upload/{filename} ์ ์
- ์๋ต ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ
401 Unauthorized
์ค์ ๊ฒฐ๊ณผ
200 OK + ํ์ผ ๋ฐ์ด๋๋ฆฌ ๋ฐํ
๊ด๋ จ ์ฝ๋ ์ปจํ
์คํธ
| File |
Role |
Relevance |
frontend/src/app/api/admin/upload/[filename]/route.ts |
admin ์
๋ก๋ ํ์ผ ์๋ธ ์๋ํฌ์ธํธ |
์์ ๋์ |
frontend/src/lib/auth/require-auth.ts |
์ธ์ฆ ํฌํผ |
์ฌ์ฌ์ฉ |
frontend/src/app/api/admin/users/route.ts |
๋์ผ ๋๋ฉ์ธ ์ฐธ์กฐ ํธ๋ค๋ฌ |
auth()+isAdmin() ํจํด ์ฐธ๊ณ |
Detected by oh-my-braincrew `omb:issue` scan
Category: api | Scan date: 2026-04-16
`omb-issue-scan category=api checklist=ISS-API-06`
์ปจํ ์คํธ ๋ธ๋ก
ISS-API-06โ Admin API routes missing role-check middleware์์ฝ
GET /api/admin/upload/[filename]๋ผ์ฐํธ์ ์ธ์ฆ ๊ฒ์ฌ๊ฐ ์ ํ ์์ด, ํ์ผ๋ช ๋ง ์๋ฉด ๋๊ตฌ๋(๋ก๊ทธ์ธ ์์ด๋) ์ ๋ก๋๋ admin ๋ธ๋๋ฉ/๋ก๊ณ ์์ฐ์ ์ง์ ๋ค์ด๋ก๋ ๊ฐ๋ฅ/api/admin/**ํธ๋ค๋ฌ๋auth()+isAdmin()๊ฐ๋๋ฅผ ๊ฑฐ์น์ง๋ง ์ด ํ์ผ ์๋ธ ๋ผ์ฐํธ๋ง ๋ฌด๋ฐฉ๋น โ ๊ด๋ฆฌ์์ฉ ๋น๊ณต๊ฐ ํ์ผ(๋ก๊ณ ์ด์, ๋ด๋ถ ์ด๋ฏธ์ง)์ด ์ธํฐ๋ท์ ๋ ธ์ถ๋จfrontend/src/app/api/admin/upload/[filename]/route.ts:24โ GET ํธ๋ค๋ฌ ๋ณธ๋ฌธEvidence
frontend/src/app/api/admin/upload/[filename]/route.tsauth()ํธ์ถ ์์, ํ์ผ ์์คํ ์์ ์ฆ์ ์ฝ์ด์ ๋ฐํfrontend/src/app/api/admin/upload/route.ts์ํฅ ๋ถ์
์ํฅ ๋ฒ์
/api/admin/upload/*๊ฒฝ๋ก๋ก ์ ์ฅ๋ ์์ฐ (๋ก๊ณ , ๋ธ๋๋ฉ ์ด๋ฏธ์ง, admin-์ ์ฉ ์ํธ์ํฌ){timestamp}-{original}.extํจํด)์ฅ์ ์๋๋ฆฌ์ค
POST /api/admin/uploadโ ํ์ผ๋ช ๋ฐํ)GET /api/admin/upload/{filename}๋ก ๋ฐ๋ก ๋ค์ด๋ก๋ ์ฑ๊ณต๊ธด๊ธ๋
์ ์ ํด๊ฒฐ ๋ฐฉ์
์ ๊ทผ ๋ฐฉ๋ฒ
frontend/src/app/api/admin/upload/[filename]/route.ts:24GET ํธ๋ค๋ฌ ์ ๋์ admin ๊ฐ๋ ์ฝ์ :์ถ๊ฐ๋ก
filename์ ๋ํ path-traversal ๋ฐฉ์ด (์ด๋ฏธ sanitize ๋์ด ์๋์ง ๊ฒ์ฆ โ../ ์ ๋๊ฒฝ๋ก ๊ฑฐ๋ถ).๋์
/api/admin/*์ ์ฒด ๋งค์ฒ๋ก middleware์์ ์ธ์ฆ ๊ฐ์ โ ๊ฐ๋ฅํ์ง๋ง ๊ธฐ์กด ํธ๋ค๋ฌ๋ค์ด ์ด๋ฏธ ํธ๋ค๋ฌ ๋ ๋ฒจ์์ ๊ฐ๋ํ๋ฏ๋ก ์ผ๊ด์ฑ ์ํด ํธ๋ค๋ฌ ๋ ๋ฒจ ์ ์ง ๊ถ์ฅ์์ฉ ๊ธฐ์ค
GET /api/admin/upload/any-file.pngํธ์ถ ์ 401 ์๋ตfrontend/e2e/๋๋frontend/src/app/api/admin/upload/[filename]/route.test.ts์์ 3๊ฐ์ง ์ผ์ด์ค ๊ฒ์ฆcd frontend && pnpm testํต๊ณผ์ฐธ์กฐ
frontend/src/app/api/admin/upload/[filename]/route.ts,frontend/src/app/api/admin/upload/route.ts,frontend/src/lib/auth/require-auth.ts์ฌํ ๋ฐฉ๋ฒ
์ฌ์ ์กฐ๊ฑด
cd frontend && pnpm dev)๋จ๊ณ
http://localhost:3000/api/admin/upload/{filename}์ ์๊ธฐ๋ ๊ฒฐ๊ณผ
401 Unauthorized
์ค์ ๊ฒฐ๊ณผ
200 OK + ํ์ผ ๋ฐ์ด๋๋ฆฌ ๋ฐํ
๊ด๋ จ ์ฝ๋ ์ปจํ ์คํธ
frontend/src/app/api/admin/upload/[filename]/route.tsfrontend/src/lib/auth/require-auth.tsfrontend/src/app/api/admin/users/route.ts