forked from svrjs/svrjs
Add URL sanitizer. Also add eslint-plugin-jest to ESLint configuration.
This commit is contained in:
parent
5171855776
commit
fbdb3f93d4
6 changed files with 320 additions and 6 deletions
|
@ -1,10 +1,30 @@
|
|||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import jest from "eslint-plugin-jest";
|
||||
|
||||
export default [
|
||||
{files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}},
|
||||
{languageOptions: { globals: {...globals.node} }},
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
sourceType: "commonjs"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tests/**/*.test.js"],
|
||||
...jest.configs['flat/recommended'],
|
||||
rules: {
|
||||
...jest.configs['flat/recommended'].rules,
|
||||
'jest/prefer-expect-assertions': 'off',
|
||||
}
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
eslintPluginPrettierRecommended
|
||||
eslintPluginPrettierRecommended,
|
||||
];
|
||||
|
|
197
package-lock.json
generated
197
package-lock.json
generated
|
@ -15,6 +15,7 @@
|
|||
"esbuild-plugin-copy": "^2.1.1",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jest": "^28.8.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"globals": "^15.9.0",
|
||||
"jest": "^29.7.0",
|
||||
|
@ -1822,6 +1823,151 @@
|
|||
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz",
|
||||
"integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.2.0",
|
||||
"@typescript-eslint/visitor-keys": "8.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz",
|
||||
"integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz",
|
||||
"integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.2.0",
|
||||
"@typescript-eslint/visitor-keys": "8.2.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz",
|
||||
"integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.2.0",
|
||||
"@typescript-eslint/types": "8.2.0",
|
||||
"@typescript-eslint/typescript-estree": "8.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz",
|
||||
"integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.2.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
|
@ -2813,6 +2959,31 @@
|
|||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jest": {
|
||||
"version": "28.8.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.0.tgz",
|
||||
"integrity": "sha512-Tubj1hooFxCl52G4qQu0edzV/+EZzPUeN8p2NnW5uu4fbDs+Yo7+qDVDc4/oG3FbCqEBmu/OC3LSsyiU22oghw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.10.0 || ^18.12.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0",
|
||||
"jest": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"optional": true
|
||||
},
|
||||
"jest": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
|
||||
|
@ -5532,6 +5703,18 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
|
||||
"integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
|
@ -5571,6 +5754,20 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"scripts": {
|
||||
"build": "node esbuild.config.js",
|
||||
"dev": "npm run build && npm run start",
|
||||
"lint": "eslint src/**/*.js",
|
||||
"lint": "eslint src/**/*.js tests/**/*.test.js",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"start": "node dist/svr.js",
|
||||
"test": "jest"
|
||||
|
@ -18,6 +18,7 @@
|
|||
"esbuild-plugin-copy": "^2.1.1",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jest": "^28.8.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"globals": "^15.9.0",
|
||||
"jest": "^29.7.0",
|
||||
|
|
42
src/utils/urlSanitizer.js
Normal file
42
src/utils/urlSanitizer.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
function sanitizeURL(resource, allowDoubleSlashes) {
|
||||
if (resource == "*" || resource == "") return resource;
|
||||
// Remove null characters
|
||||
resource = resource.replace(/%00|\0/g, "");
|
||||
// Check if URL is malformed (e.g. %c0%af or %u002f or simply %as)
|
||||
if (resource.match(/%(?:c[01]|f[ef]|(?![0-9a-f]{2}).{2}|.{0,1}$)/i))
|
||||
throw new URIError("URI malformed");
|
||||
// Decode URL-encoded characters while preserving certain characters
|
||||
resource = resource.replace(/%([0-9a-f]{2})/gi, (match, hex) => {
|
||||
var decodedChar = String.fromCharCode(parseInt(hex, 16));
|
||||
return /(?!["<>^`{|}?#%])[!-~]/.test(decodedChar) ? decodedChar : "%" + hex;
|
||||
});
|
||||
// Encode certain characters
|
||||
resource = resource.replace(/[<>^`{|}]]/g, (character) => {
|
||||
var charCode = character.charCodeAt(0);
|
||||
return (
|
||||
"%" + (charCode < 16 ? "0" : "") + charCode.toString(16).toUpperCase()
|
||||
);
|
||||
});
|
||||
var sanitizedResource = resource;
|
||||
// Ensure the resource starts with a slash
|
||||
if (resource[0] != "/") sanitizedResource = "/" + sanitizedResource;
|
||||
// Convert backslashes to slashes and handle duplicate slashes
|
||||
sanitizedResource = sanitizedResource
|
||||
.replace(/\\/g, "/")
|
||||
.replace(allowDoubleSlashes ? /\/{3,}/g : /\/+/g, "/");
|
||||
// Handle relative navigation (e.g., "/./", "/../", "../", "./"), also remove trailing dots in paths
|
||||
sanitizedResource = sanitizedResource
|
||||
.replace(/\/\.(?:\.{2,})?(?=\/|$)/g, "")
|
||||
.replace(/([^.\/])\.+(?=\/|$)/g, "$1");
|
||||
while (sanitizedResource.match(/\/(?!\.\.\/)[^\/]+\/\.\.(?=\/|$)/)) {
|
||||
sanitizedResource = sanitizedResource.replace(
|
||||
/\/(?!\.\.\/)[^\/]+\/\.\.(?=\/|$)/g,
|
||||
"",
|
||||
);
|
||||
}
|
||||
sanitizedResource = sanitizedResource.replace(/\/\.\.(?=\/|$)/g, "");
|
||||
if (sanitizedResource.length == 0) return "/";
|
||||
else return sanitizedResource;
|
||||
}
|
||||
|
||||
module.exports = sanitizeURL;
|
|
@ -1,5 +1,5 @@
|
|||
const { add } = require('../../src/utils/helper');
|
||||
const { add } = require("../../src/utils/helper");
|
||||
|
||||
test('adds 1 + 2 to equal 3', () => {
|
||||
test("adds 1 + 2 to equal 3", () => {
|
||||
expect(add(1, 2)).toBe(3);
|
||||
});
|
||||
|
|
54
tests/utils/urlSanitizer.test.js
Normal file
54
tests/utils/urlSanitizer.test.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
const sanitizeURL = require("../../src/utils/urlSanitizer.js");
|
||||
|
||||
describe("URL sanitizer", () => {
|
||||
test('should return "*" for "*"', () => {
|
||||
expect(sanitizeURL("*")).toBe("*");
|
||||
});
|
||||
|
||||
test("should return empty string for empty string", () => {
|
||||
expect(sanitizeURL("")).toBe("");
|
||||
});
|
||||
|
||||
test("should remove null characters", () => {
|
||||
expect(sanitizeURL("/test%00")).toBe("/test");
|
||||
expect(sanitizeURL("/test\0")).toBe("/test");
|
||||
});
|
||||
|
||||
test("should throw URIError for malformed URL", () => {
|
||||
expect(() => sanitizeURL("%c0%af")).toThrow(URIError);
|
||||
expect(() => sanitizeURL("%u002f")).toThrow(URIError);
|
||||
expect(() => sanitizeURL("%as")).toThrow(URIError);
|
||||
});
|
||||
|
||||
test("should ensure the resource starts with a slash", () => {
|
||||
expect(sanitizeURL("test")).toBe("/test");
|
||||
});
|
||||
|
||||
test("should convert backslashes to slashes", () => {
|
||||
expect(sanitizeURL("test\\path")).toBe("/test/path");
|
||||
});
|
||||
|
||||
test("should handle duplicate slashes", () => {
|
||||
expect(sanitizeURL("test//path", false)).toBe("/test/path");
|
||||
expect(sanitizeURL("test//path", true)).toBe("/test//path");
|
||||
});
|
||||
|
||||
test("should handle relative navigation", () => {
|
||||
expect(sanitizeURL("/./test")).toBe("/test");
|
||||
expect(sanitizeURL("/../test")).toBe("/test");
|
||||
expect(sanitizeURL("../test")).toBe("/test");
|
||||
expect(sanitizeURL("./test")).toBe("/test");
|
||||
expect(sanitizeURL("/test/./")).toBe("/test/");
|
||||
expect(sanitizeURL("/test/../")).toBe("/");
|
||||
expect(sanitizeURL("/test/../path")).toBe("/path");
|
||||
});
|
||||
|
||||
test("should remove trailing dots in paths", () => {
|
||||
expect(sanitizeURL("/test...")).toBe("/test");
|
||||
expect(sanitizeURL("/test.../")).toBe("/test/");
|
||||
});
|
||||
|
||||
test('should return "/" for empty sanitized resource', () => {
|
||||
expect(sanitizeURL("/../..")).toBe("/");
|
||||
});
|
||||
});
|
Reference in a new issue