webpack-dev-middleware
Advanced tools
Comparing version 6.1.1 to 6.1.2
@@ -12,3 +12,4 @@ "use strict"; | ||
setStatusCode, | ||
send | ||
send, | ||
sendError | ||
} = require("./utils/compatibleAPI"); | ||
@@ -75,2 +76,4 @@ const ready = require("./utils/ready"); | ||
async function processRequest() { | ||
/** @type {import("./utils/getFilenameFromUrl").Extra} */ | ||
const extra = {}; | ||
const filename = getFilenameFromUrl(context, /** @type {string} */req.url); | ||
@@ -81,2 +84,11 @@ if (!filename) { | ||
} | ||
if (extra.errorCode) { | ||
if (extra.errorCode === 403) { | ||
context.logger.error(`Malicious path "${filename}".`); | ||
} | ||
sendError(req, res, extra.errorCode, { | ||
modifyResponseData: context.options.modifyResponseData | ||
}); | ||
return; | ||
} | ||
let { | ||
@@ -83,0 +95,0 @@ headers |
@@ -137,2 +137,144 @@ "use strict"; | ||
} | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
*/ | ||
function clearHeadersForResponse(res) { | ||
const headers = getHeaderNames(res); | ||
for (let i = 0; i < headers.length; i++) { | ||
res.removeHeader(headers[i]); | ||
} | ||
} | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
* @param {Record<string, number | string | string[] | undefined>} headers | ||
*/ | ||
function setHeadersForResponse(res, headers) { | ||
const keys = Object.keys(headers); | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
const value = headers[key]; | ||
if (typeof value !== "undefined") { | ||
setHeaderForResponse(res, key, value); | ||
} | ||
} | ||
} | ||
const matchHtmlRegExp = /["'&<>]/; | ||
/** | ||
* @param {string} string raw HTML | ||
* @returns {string} escaped HTML | ||
*/ | ||
function escapeHtml(string) { | ||
const str = `${string}`; | ||
const match = matchHtmlRegExp.exec(str); | ||
if (!match) { | ||
return str; | ||
} | ||
let escape; | ||
let html = ""; | ||
let index = 0; | ||
let lastIndex = 0; | ||
for (({ | ||
index | ||
} = match); index < str.length; index++) { | ||
switch (str.charCodeAt(index)) { | ||
// " | ||
case 34: | ||
escape = """; | ||
break; | ||
// & | ||
case 38: | ||
escape = "&"; | ||
break; | ||
// ' | ||
case 39: | ||
escape = "'"; | ||
break; | ||
// < | ||
case 60: | ||
escape = "<"; | ||
break; | ||
// > | ||
case 62: | ||
escape = ">"; | ||
break; | ||
default: | ||
// eslint-disable-next-line no-continue | ||
continue; | ||
} | ||
if (lastIndex !== index) { | ||
html += str.substring(lastIndex, index); | ||
} | ||
lastIndex = index + 1; | ||
html += escape; | ||
} | ||
return lastIndex !== index ? html + str.substring(lastIndex, index) : html; | ||
} | ||
/** @type {Record<number, string>} */ | ||
const statuses = { | ||
400: "Bad Request", | ||
403: "Forbidden", | ||
404: "Not Found", | ||
416: "Range Not Satisfiable", | ||
500: "Internal Server Error" | ||
}; | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @typedef {Object} SendOptions send error options | ||
* @property {Record<string, number | string | string[] | undefined>=} headers headers | ||
* @property {import("../index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback | ||
* @property {import("../index").OutputFileSystem} outputFileSystem modify response data callback | ||
*/ | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @param {Request} req response | ||
* @param {Response} res response | ||
* @param {number} status status | ||
* @param {Partial<SendOptions<Request, Response>>=} options options | ||
* @returns {void} | ||
*/ | ||
function sendError(req, res, status, options) { | ||
const content = statuses[status] || String(status); | ||
let document = `<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Error</title> | ||
</head> | ||
<body> | ||
<pre>${escapeHtml(content)}</pre> | ||
</body> | ||
</html>`; | ||
// Clear existing headers | ||
clearHeadersForResponse(res); | ||
if (options && options.headers) { | ||
setHeadersForResponse(res, options.headers); | ||
} | ||
// Send basic response | ||
setStatusCode(res, status); | ||
setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8"); | ||
setHeaderForResponse(res, "Content-Security-Policy", "default-src 'none'"); | ||
setHeaderForResponse(res, "X-Content-Type-Options", "nosniff"); | ||
let byteLength = Buffer.byteLength(document); | ||
if (options && options.modifyResponseData) { | ||
({ | ||
data: document, | ||
byteLength | ||
} = /** @type {{data: string, byteLength: number }} */ | ||
options.modifyResponseData(req, res, document, byteLength)); | ||
} | ||
setHeaderForResponse(res, "Content-Length", byteLength); | ||
res.end(document); | ||
} | ||
module.exports = { | ||
@@ -144,3 +286,4 @@ getHeaderNames, | ||
setStatusCode, | ||
send | ||
send, | ||
sendError | ||
}; |
@@ -16,4 +16,6 @@ "use strict"; | ||
/** | ||
* @template T | ||
* @param {Function} fn | ||
* @param {{ cache?: Map<any, any> }} [cache] | ||
* @param {{ cache?: Map<string, { data: T }> } | undefined} cache | ||
* @param {(value: T) => T} callback | ||
* @returns {any} | ||
@@ -24,3 +26,3 @@ */ | ||
cache = new Map() | ||
} = {}) => { | ||
} = {}, callback) => { | ||
/** | ||
@@ -36,3 +38,4 @@ * @param {any} arguments_ | ||
} | ||
const result = fn.apply(void 0, arguments_); | ||
let result = fn.apply(void 0, arguments_); | ||
result = callback(result); | ||
cache.set(key, { | ||
@@ -46,5 +49,32 @@ data: result | ||
}; | ||
const memoizedParse = mem(parse); | ||
// eslint-disable-next-line no-undefined | ||
const memoizedParse = mem(parse, undefined, value => { | ||
if (value.pathname) { | ||
// eslint-disable-next-line no-param-reassign | ||
value.pathname = decode(value.pathname); | ||
} | ||
return value; | ||
}); | ||
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/; | ||
/** | ||
* @typedef {Object} Extra | ||
* @property {import("fs").Stats=} stats | ||
* @property {number=} errorCode | ||
*/ | ||
/** | ||
* decodeURIComponent. | ||
* | ||
* Allows V8 to only deoptimize this fn instead of all of send(). | ||
* | ||
* @param {string} input | ||
* @returns {string} | ||
*/ | ||
function decode(input) { | ||
return querystring.unescape(input); | ||
} | ||
/** | ||
* @template {IncomingMessage} Request | ||
@@ -54,5 +84,6 @@ * @template {ServerResponse} Response | ||
* @param {string} url | ||
* @param {Extra=} extra | ||
* @returns {string | undefined} | ||
*/ | ||
function getFilenameFromUrl(context, url) { | ||
function getFilenameFromUrl(context, url, extra = {}) { | ||
const { | ||
@@ -62,3 +93,6 @@ options | ||
const paths = getPaths(context); | ||
/** @type {string | undefined} */ | ||
let foundFilename; | ||
/** @type {URL} */ | ||
let urlObject; | ||
@@ -75,3 +109,5 @@ try { | ||
} of paths) { | ||
/** @type {string | undefined} */ | ||
let filename; | ||
/** @type {URL} */ | ||
let publicPathObject; | ||
@@ -84,14 +120,31 @@ try { | ||
} | ||
if (urlObject.pathname && urlObject.pathname.startsWith(publicPathObject.pathname)) { | ||
filename = outputPath; | ||
const { | ||
pathname | ||
} = urlObject; | ||
const { | ||
pathname: publicPathPathname | ||
} = publicPathObject; | ||
if (pathname && pathname.startsWith(publicPathPathname)) { | ||
// Null byte(s) | ||
if (pathname.includes("\0")) { | ||
// eslint-disable-next-line no-param-reassign | ||
extra.errorCode = 400; | ||
return; | ||
} | ||
// ".." is malicious | ||
if (UP_PATH_REGEXP.test(path.normalize(`./${pathname}`))) { | ||
// eslint-disable-next-line no-param-reassign | ||
extra.errorCode = 403; | ||
return; | ||
} | ||
// Strip the `pathname` property from the `publicPath` option from the start of requested url | ||
// `/complex/foo.js` => `foo.js` | ||
const pathname = urlObject.pathname.slice(publicPathObject.pathname.length); | ||
if (pathname) { | ||
filename = path.join(outputPath, querystring.unescape(pathname)); | ||
} | ||
let fsStats; | ||
// and add outputPath | ||
// `foo.js` => `/home/user/my-project/dist/foo.js` | ||
filename = path.join(outputPath, pathname.slice(publicPathPathname.length)); | ||
try { | ||
fsStats = /** @type {import("fs").statSync} */ | ||
// eslint-disable-next-line no-param-reassign | ||
extra.stats = /** @type {import("fs").statSync} */ | ||
context.outputFileSystem.statSync(filename); | ||
@@ -102,10 +155,10 @@ } catch (_ignoreError) { | ||
} | ||
if (fsStats.isFile()) { | ||
if (extra.stats.isFile()) { | ||
foundFilename = filename; | ||
break; | ||
} else if (fsStats.isDirectory() && (typeof options.index === "undefined" || options.index)) { | ||
} else if (extra.stats.isDirectory() && (typeof options.index === "undefined" || options.index)) { | ||
const indexValue = typeof options.index === "undefined" || typeof options.index === "boolean" ? "index.html" : options.index; | ||
filename = path.join(filename, indexValue); | ||
try { | ||
fsStats = /** @type {import("fs").statSync} */ | ||
extra.stats = /** @type {import("fs").statSync} */ | ||
context.outputFileSystem.statSync(filename); | ||
@@ -116,3 +169,3 @@ } catch (__ignoreError) { | ||
} | ||
if (fsStats.isFile()) { | ||
if (extra.stats.isFile()) { | ||
foundFilename = filename; | ||
@@ -119,0 +172,0 @@ break; |
{ | ||
"name": "webpack-dev-middleware", | ||
"version": "6.1.1", | ||
"version": "6.1.2", | ||
"description": "A development middleware for webpack", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -13,2 +13,24 @@ /// <reference types="node" /> | ||
}; | ||
/** | ||
* send error options | ||
*/ | ||
export type SendOptions< | ||
Request_1 extends import("http").IncomingMessage, | ||
Response_1 extends import("../index.js").ServerResponse | ||
> = { | ||
/** | ||
* headers | ||
*/ | ||
headers?: Record<string, number | string | string[] | undefined> | undefined; | ||
/** | ||
* modify response data callback | ||
*/ | ||
modifyResponseData?: | ||
| import("../index").ModifyResponseData<Request, Response> | ||
| undefined; | ||
/** | ||
* modify response data callback | ||
*/ | ||
outputFileSystem: import("../index").OutputFileSystem; | ||
}; | ||
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ | ||
@@ -88,1 +110,27 @@ /** @typedef {import("../index.js").ServerResponse} ServerResponse */ | ||
): void; | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @typedef {Object} SendOptions send error options | ||
* @property {Record<string, number | string | string[] | undefined>=} headers headers | ||
* @property {import("../index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback | ||
* @property {import("../index").OutputFileSystem} outputFileSystem modify response data callback | ||
*/ | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @param {Request} req response | ||
* @param {Response} res response | ||
* @param {number} status status | ||
* @param {Partial<SendOptions<Request, Response>>=} options options | ||
* @returns {void} | ||
*/ | ||
export function sendError< | ||
Request_1 extends import("http").IncomingMessage, | ||
Response_1 extends import("../index.js").ServerResponse | ||
>( | ||
req: Request_1, | ||
res: Response_1, | ||
status: number, | ||
options?: Partial<SendOptions<Request_1, Response_1>> | undefined | ||
): void; |
@@ -8,2 +8,3 @@ /// <reference types="node" /> | ||
* @param {string} url | ||
* @param {Extra=} extra | ||
* @returns {string | undefined} | ||
@@ -16,8 +17,13 @@ */ | ||
context: import("../index.js").Context<Request_1, Response_1>, | ||
url: string | ||
url: string, | ||
extra?: Extra | undefined | ||
): string | undefined; | ||
declare namespace getFilenameFromUrl { | ||
export { IncomingMessage, ServerResponse }; | ||
export { Extra, IncomingMessage, ServerResponse }; | ||
} | ||
type Extra = { | ||
stats?: import("fs").Stats | undefined; | ||
errorCode?: number | undefined; | ||
}; | ||
type IncomingMessage = import("../index.js").IncomingMessage; | ||
type ServerResponse = import("../index.js").ServerResponse; |
89251
1962