mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
fix(security): remove regex validation backtracking path
This commit is contained in:
@@ -7,17 +7,6 @@ export interface ValidationResult {
|
||||
fallbackApplied: boolean;
|
||||
}
|
||||
|
||||
function collectMatches(text: string, pattern: RegExp): string[] {
|
||||
const matches: string[] = [];
|
||||
pattern.lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
if (match[0]) matches.push(match[0]);
|
||||
if (match.index === pattern.lastIndex) pattern.lastIndex++;
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function requireExactPresence(
|
||||
label: string,
|
||||
originalItems: string[],
|
||||
@@ -43,6 +32,41 @@ function isPreviewWhitespace(char: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isHorizontalWhitespace(char: string): boolean {
|
||||
return char === " " || char === "\t";
|
||||
}
|
||||
|
||||
function isAsciiDigit(char: string | undefined): boolean {
|
||||
return !!char && char >= "0" && char <= "9";
|
||||
}
|
||||
|
||||
function isAsciiUpper(char: string | undefined): boolean {
|
||||
return !!char && char >= "A" && char <= "Z";
|
||||
}
|
||||
|
||||
function isAsciiLetter(char: string | undefined): boolean {
|
||||
return !!char && ((char >= "A" && char <= "Z") || (char >= "a" && char <= "z"));
|
||||
}
|
||||
|
||||
function isAsciiAlphaNumeric(char: string | undefined): boolean {
|
||||
return !!char && (isAsciiLetter(char) || isAsciiDigit(char));
|
||||
}
|
||||
|
||||
function isWordChar(char: string | undefined): boolean {
|
||||
return !!char && (isAsciiAlphaNumeric(char) || char === "_");
|
||||
}
|
||||
|
||||
function isUrlTerminator(char: string): boolean {
|
||||
return (
|
||||
isPreviewWhitespace(char) ||
|
||||
char === ")" ||
|
||||
char === "]" ||
|
||||
char === '"' ||
|
||||
char === "'" ||
|
||||
char === ">"
|
||||
);
|
||||
}
|
||||
|
||||
function collapseWhitespaceForPreview(text: string): string {
|
||||
let output = "";
|
||||
let previousWasWhitespace = false;
|
||||
@@ -85,57 +109,17 @@ export function validateCompression(original: string, compressed: string): Valid
|
||||
}
|
||||
|
||||
requireExactPresence("fenced code block", findFencedCodeBlocks(original), compressed, errors);
|
||||
requireExactPresence("inline code", collectMatches(original, /`[^`\n]+`/g), compressed, errors);
|
||||
requireExactPresence(
|
||||
"URL",
|
||||
collectMatches(original, /\bhttps?:\/\/[^\s)\]"'>]+/gi),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
requireExactPresence(
|
||||
"markdown link",
|
||||
collectMatches(original, /\[[^\]\n]{1,1000}\]\([^)\n]{1,2000}\)/g),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
requireExactPresence("inline code", collectInlineCode(original), compressed, errors);
|
||||
requireExactPresence("URL", collectUrls(original), compressed, errors);
|
||||
requireExactPresence("markdown link", collectMarkdownLinks(original), compressed, errors);
|
||||
requireExactPresence("frontmatter", collectFrontmatter(original), compressed, errors);
|
||||
requireExactPresence("heading", collectMatches(original, /^#{1,6}\s+.+$/gm), compressed, errors);
|
||||
requireExactPresence(
|
||||
"table row",
|
||||
collectMatches(original, /^[ \t]*\|(?:[^|\n]{0,1000}\|){1,100}/gm),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
requireExactPresence(
|
||||
"math block",
|
||||
collectMatches(original, /\$\$[\s\S]{0,10000}?\$\$/g),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
requireExactPresence(
|
||||
"inline math",
|
||||
collectMatches(original, /(?<!\$)\$(?![\s$\d])(?:\\.|[^$\n]){1,160}?(?<!\s)\$(?!\$)/g),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
requireExactPresence(
|
||||
"LaTeX block",
|
||||
collectMatches(original, /\\begin\{[A-Za-z*]{1,50}\}[\s\S]{0,10000}?\\end\{[A-Za-z*]{1,50}\}/g),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
requireExactPresence(
|
||||
"version",
|
||||
collectMatches(original, /\b\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?\b/g),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
requireExactPresence(
|
||||
"CONST_CASE",
|
||||
collectMatches(original, /\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
requireExactPresence("heading", collectHeadings(original), compressed, errors);
|
||||
requireExactPresence("table row", collectTableRows(original), compressed, errors);
|
||||
requireExactPresence("math block", collectMathBlocks(original), compressed, errors);
|
||||
requireExactPresence("inline math", collectInlineMath(original), compressed, errors);
|
||||
requireExactPresence("LaTeX block", collectLatexBlocks(original), compressed, errors);
|
||||
requireExactPresence("version", collectVersions(original), compressed, errors);
|
||||
requireExactPresence("CONST_CASE", collectConstCase(original), compressed, errors);
|
||||
|
||||
const originalFenceCount = findFencedCodeBlocks(original).length;
|
||||
const compressedFenceCount = findFencedCodeBlocks(compressed).length;
|
||||
@@ -157,6 +141,78 @@ export function validateCompression(original: string, compressed: string): Valid
|
||||
};
|
||||
}
|
||||
|
||||
function collectInlineCode(text: string): string[] {
|
||||
const matches: string[] = [];
|
||||
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
if (text[index] !== "`") continue;
|
||||
const end = text.indexOf("`", index + 1);
|
||||
if (end === -1) break;
|
||||
|
||||
const content = text.slice(index + 1, end);
|
||||
if (content.length > 0 && !content.includes("\n")) {
|
||||
matches.push(text.slice(index, end + 1));
|
||||
index = end;
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function collectUrls(text: string): string[] {
|
||||
const matches: string[] = [];
|
||||
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
const startsWithHttp = text.startsWith("http://", index);
|
||||
const startsWithHttps = text.startsWith("https://", index);
|
||||
if (!startsWithHttp && !startsWithHttps) continue;
|
||||
if (index > 0 && isWordChar(text[index - 1])) continue;
|
||||
|
||||
let end = index + (startsWithHttps ? "https://".length : "http://".length);
|
||||
while (end < text.length && !isUrlTerminator(text[end])) {
|
||||
end++;
|
||||
}
|
||||
|
||||
if (end > index) {
|
||||
matches.push(text.slice(index, end));
|
||||
index = end - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function collectMarkdownLinks(text: string): string[] {
|
||||
const matches: string[] = [];
|
||||
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
if (text[index] !== "[") continue;
|
||||
|
||||
const labelEnd = text.indexOf("]", index + 1);
|
||||
if (labelEnd === -1 || labelEnd - index - 1 > 1000) continue;
|
||||
if (text.slice(index + 1, labelEnd).includes("\n")) continue;
|
||||
if (text[labelEnd + 1] !== "(") continue;
|
||||
|
||||
const targetStart = labelEnd + 2;
|
||||
const maxTargetEnd = Math.min(text.length, targetStart + 2000);
|
||||
let targetEnd = -1;
|
||||
for (let cursor = targetStart; cursor < maxTargetEnd; cursor++) {
|
||||
if (text[cursor] === "\n") break;
|
||||
if (text[cursor] === ")") {
|
||||
targetEnd = cursor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetEnd !== -1) {
|
||||
matches.push(text.slice(index, targetEnd + 1));
|
||||
index = targetEnd;
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function collectFrontmatter(text: string): string[] {
|
||||
if (!text.startsWith("---\n")) return [];
|
||||
const close = text.indexOf("\n---", 4);
|
||||
@@ -165,3 +221,212 @@ function collectFrontmatter(text: string): string[] {
|
||||
const end = closeEnd === -1 ? text.length : closeEnd + 1;
|
||||
return [text.slice(0, end)];
|
||||
}
|
||||
|
||||
function collectHeadings(text: string): string[] {
|
||||
return collectLines(text).filter((line) => {
|
||||
let markerCount = 0;
|
||||
while (markerCount < line.length && line[markerCount] === "#") {
|
||||
markerCount++;
|
||||
}
|
||||
if (markerCount < 1 || markerCount > 6) return false;
|
||||
if (!isPreviewWhitespace(line[markerCount] ?? "")) return false;
|
||||
|
||||
for (let index = markerCount + 1; index < line.length; index++) {
|
||||
if (!isPreviewWhitespace(line[index])) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function collectTableRows(text: string): string[] {
|
||||
return collectLines(text).filter((line) => {
|
||||
let index = 0;
|
||||
while (index < line.length && isHorizontalWhitespace(line[index])) {
|
||||
index++;
|
||||
}
|
||||
if (line[index] !== "|") return false;
|
||||
|
||||
let pipeCount = 0;
|
||||
for (; index < line.length; index++) {
|
||||
if (line[index] === "|") pipeCount++;
|
||||
}
|
||||
return pipeCount >= 2;
|
||||
});
|
||||
}
|
||||
|
||||
function collectLines(text: string): string[] {
|
||||
const lines: string[] = [];
|
||||
let start = 0;
|
||||
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
if (text[index] !== "\n") continue;
|
||||
lines.push(text.slice(start, index));
|
||||
start = index + 1;
|
||||
}
|
||||
|
||||
lines.push(text.slice(start));
|
||||
return lines;
|
||||
}
|
||||
|
||||
function collectMathBlocks(text: string): string[] {
|
||||
const matches: string[] = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < text.length) {
|
||||
const start = text.indexOf("$$", index);
|
||||
if (start === -1) break;
|
||||
const end = text.indexOf("$$", start + 2);
|
||||
if (end === -1) break;
|
||||
|
||||
if (end - start - 2 <= 10000) {
|
||||
matches.push(text.slice(start, end + 2));
|
||||
index = end + 2;
|
||||
} else {
|
||||
index = start + 2;
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function collectInlineMath(text: string): string[] {
|
||||
const matches: string[] = [];
|
||||
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
if (text[index] !== "$") continue;
|
||||
if (text[index - 1] === "$") continue;
|
||||
|
||||
const next = text[index + 1];
|
||||
if (!next || isPreviewWhitespace(next) || next === "$" || isAsciiDigit(next)) continue;
|
||||
|
||||
const maxEnd = Math.min(text.length, index + 1 + 160);
|
||||
for (let cursor = index + 1; cursor < maxEnd; cursor++) {
|
||||
const char = text[cursor];
|
||||
if (char === "\n") break;
|
||||
if (char === "\\") {
|
||||
cursor++;
|
||||
continue;
|
||||
}
|
||||
if (char !== "$") continue;
|
||||
if (isPreviewWhitespace(text[cursor - 1] ?? "")) continue;
|
||||
if (text[cursor + 1] === "$") continue;
|
||||
|
||||
matches.push(text.slice(index, cursor + 1));
|
||||
index = cursor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function collectLatexBlocks(text: string): string[] {
|
||||
const matches: string[] = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < text.length) {
|
||||
const start = text.indexOf("\\begin{", index);
|
||||
if (start === -1) break;
|
||||
|
||||
const envStart = start + "\\begin{".length;
|
||||
const envEnd = text.indexOf("}", envStart);
|
||||
if (envEnd === -1 || envEnd - envStart < 1 || envEnd - envStart > 50) {
|
||||
index = envStart;
|
||||
continue;
|
||||
}
|
||||
|
||||
const env = text.slice(envStart, envEnd);
|
||||
if (![...env].every((char) => isAsciiLetter(char) || char === "*")) {
|
||||
index = envStart;
|
||||
continue;
|
||||
}
|
||||
|
||||
const closeToken = `\\end{${env}}`;
|
||||
const closeStart = text.indexOf(closeToken, envEnd + 1);
|
||||
if (closeStart === -1) {
|
||||
index = envEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (closeStart - envEnd - 1 <= 10000) {
|
||||
const end = closeStart + closeToken.length;
|
||||
matches.push(text.slice(start, end));
|
||||
index = end;
|
||||
} else {
|
||||
index = envEnd + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function collectVersions(text: string): string[] {
|
||||
const matches: string[] = [];
|
||||
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
if (!isAsciiDigit(text[index])) continue;
|
||||
if (index > 0 && isWordChar(text[index - 1])) continue;
|
||||
|
||||
let cursor = readDigits(text, index);
|
||||
let dotGroups = 0;
|
||||
while (dotGroups < 3 && text[cursor] === "." && isAsciiDigit(text[cursor + 1])) {
|
||||
cursor = readDigits(text, cursor + 1);
|
||||
dotGroups++;
|
||||
}
|
||||
if (dotGroups < 1) continue;
|
||||
|
||||
if ((text[cursor] === "-" || text[cursor] === "+") && isVersionSuffixChar(text[cursor + 1])) {
|
||||
cursor++;
|
||||
while (cursor < text.length && isVersionSuffixChar(text[cursor])) {
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
if (isWordChar(text[cursor])) continue;
|
||||
matches.push(text.slice(index, cursor));
|
||||
index = cursor - 1;
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function readDigits(text: string, start: number): number {
|
||||
let cursor = start;
|
||||
while (cursor < text.length && isAsciiDigit(text[cursor])) {
|
||||
cursor++;
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
function isVersionSuffixChar(char: string | undefined): boolean {
|
||||
return !!char && (isAsciiAlphaNumeric(char) || char === "." || char === "-");
|
||||
}
|
||||
|
||||
function collectConstCase(text: string): string[] {
|
||||
const matches: string[] = [];
|
||||
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
if (!isAsciiUpper(text[index])) continue;
|
||||
if (index > 0 && isWordChar(text[index - 1])) continue;
|
||||
|
||||
let cursor = index + 1;
|
||||
let hasUnderscore = false;
|
||||
while (cursor < text.length) {
|
||||
const char = text[cursor];
|
||||
if (char === "_") {
|
||||
if (!isAsciiUpper(text[cursor + 1]) && !isAsciiDigit(text[cursor + 1])) break;
|
||||
hasUnderscore = true;
|
||||
cursor++;
|
||||
continue;
|
||||
}
|
||||
if (!isAsciiUpper(char) && !isAsciiDigit(char)) break;
|
||||
cursor++;
|
||||
}
|
||||
|
||||
if (!hasUnderscore || isWordChar(text[cursor])) continue;
|
||||
matches.push(text.slice(index, cursor));
|
||||
index = cursor - 1;
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user