1
0
Fork 0
forked from svrjs/svrjs

Add URL sanitizer. Also add eslint-plugin-jest to ESLint configuration.

This commit is contained in:
Dorian Niemiec 2024-08-23 17:43:37 +02:00
parent 5171855776
commit fbdb3f93d4
6 changed files with 320 additions and 6 deletions

View file

@ -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
View file

@ -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",

View file

@ -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
View 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;

View file

@ -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);
});

View 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("/");
});
});