110 lines
5.2 KiB
JavaScript
110 lines
5.2 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
Object.defineProperty(exports, "blockCrossSiteDEV", {
|
|
enumerable: true,
|
|
get: function() {
|
|
return blockCrossSiteDEV;
|
|
}
|
|
});
|
|
const _url = require("../../../lib/url");
|
|
const _log = require("../../../build/output/log");
|
|
const _csrfprotection = require("../../app-render/csrf-protection");
|
|
const allowedDevOriginsDocs = 'https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins';
|
|
function getBlockedResourcePath(req) {
|
|
var _parseUrl;
|
|
return ((_parseUrl = (0, _url.parseUrl)(req.url ?? '')) == null ? void 0 : _parseUrl.pathname) ?? req.url ?? '/_next/*';
|
|
}
|
|
function formatBlockedCrossSiteMessage(source, resourcePath) {
|
|
const lines = [
|
|
`Blocked cross-origin request to Next.js dev resource ${resourcePath}${getBlockedSourceDescription(source)}.`,
|
|
'Cross-origin access to Next.js dev resources is blocked by default for safety.'
|
|
];
|
|
// `source` has 3 meanings here:
|
|
// - `'null'`: browser explicitly sent `Origin: null` for an opaque/sandboxed origin
|
|
// - hostname string: we parsed an allowlistable host from Origin/Referer
|
|
// - `undefined` (and effectively empty string): the request did not include a usable host
|
|
if (source === 'null') {
|
|
lines.push('', 'This request came from a privacy-sensitive or opaque origin, so Next.js cannot determine which host to allow.', 'If you need it to succeed, load the dev server from a normal origin and add that host to "allowedDevOrigins".');
|
|
} else if (source) {
|
|
lines.push('', 'To allow this host in development, add it to "allowedDevOrigins" in next.config.js and restart the dev server:', '', '// next.config.js', 'module.exports = {', ` allowedDevOrigins: ['${source}'],`, '}');
|
|
} else {
|
|
lines.push('', 'This request did not include an allowlistable source host.', 'If you need it to succeed, make sure the browser sends an Origin or Referer from a host listed in "allowedDevOrigins".');
|
|
}
|
|
lines.push('', `Read more: ${allowedDevOriginsDocs}`);
|
|
return lines.join('\n');
|
|
}
|
|
function getBlockedSourceDescription(source) {
|
|
if (source === 'null') {
|
|
return ' from a privacy-sensitive or opaque origin';
|
|
}
|
|
if (source) {
|
|
return ` from "${source}"`;
|
|
}
|
|
return ' from an unknown source';
|
|
}
|
|
function blockRequest(req, res, source) {
|
|
(0, _log.warnOnce)(formatBlockedCrossSiteMessage(source, getBlockedResourcePath(req)));
|
|
if ('statusCode' in res) {
|
|
res.statusCode = 403;
|
|
}
|
|
res.end('Unauthorized');
|
|
return true;
|
|
}
|
|
function parseHostnameFromHeader(header) {
|
|
const headerValue = Array.isArray(header) ? header[0] : header;
|
|
if (!headerValue || headerValue === 'null') {
|
|
return;
|
|
}
|
|
const parsedHeader = (0, _url.parseUrl)(headerValue);
|
|
return parsedHeader == null ? void 0 : parsedHeader.hostname.toLowerCase();
|
|
}
|
|
function isInternalEndpoint(req) {
|
|
if (!req.url) return false;
|
|
try {
|
|
// TODO: We should standardize on a single prefix for this
|
|
const isMiddlewareRequest = req.url.includes('/__nextjs');
|
|
const isInternalAsset = req.url.includes('/_next');
|
|
// Static media requests are excluded, as they might be loaded via CSS and would fail
|
|
// CORS checks.
|
|
const isIgnoredRequest = req.url.includes('/_next/image') || req.url.includes('/_next/static/media');
|
|
return !isIgnoredRequest && (isInternalAsset || isMiddlewareRequest);
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
const blockCrossSiteDEV = (req, res, allowedDevOrigins, hostname)=>{
|
|
const allowedOrigins = [
|
|
'*.localhost',
|
|
'localhost',
|
|
...allowedDevOrigins ?? []
|
|
];
|
|
if (hostname) {
|
|
allowedOrigins.push(hostname);
|
|
}
|
|
// only process internal URLs/middleware
|
|
if (!isInternalEndpoint(req)) {
|
|
return false;
|
|
}
|
|
// block non-cors request from cross-site e.g. script tag on
|
|
// different host
|
|
if (req.headers['sec-fetch-mode'] === 'no-cors' && req.headers['sec-fetch-site'] === 'cross-site') {
|
|
// no-cors requests do not send an Origin header, so fall back to Referer
|
|
// when validating configured cross-site script loads.
|
|
const refererHostname = parseHostnameFromHeader(req.headers['referer']);
|
|
if (refererHostname && (0, _csrfprotection.isCsrfOriginAllowed)(refererHostname, allowedOrigins)) {
|
|
return false;
|
|
}
|
|
return blockRequest(req, res, refererHostname);
|
|
}
|
|
// ensure websocket requests are only fulfilled from allowed origin
|
|
const rawOrigin = req.headers['origin'];
|
|
const originHeader = Array.isArray(rawOrigin) ? rawOrigin[0] : rawOrigin;
|
|
const parsedOrigin = originHeader && originHeader !== 'null' ? (0, _url.parseUrl)(originHeader) : originHeader;
|
|
const originLowerCase = parsedOrigin === undefined || typeof parsedOrigin === 'string' ? parsedOrigin : parsedOrigin.hostname.toLowerCase();
|
|
// Allow requests with no origin since those are just GET requests from same-site
|
|
return originLowerCase !== undefined && !(0, _csrfprotection.isCsrfOriginAllowed)(originLowerCase, allowedOrigins) && blockRequest(req, res, originLowerCase);
|
|
};
|
|
|
|
//# sourceMappingURL=block-cross-site-dev.js.map
|