nehonix-uri-processor
Version:
A powerful URI processor for encoding, decoding, and analyzing URI data securely.
750 lines • 28.9 kB
JavaScript
import { AppLogger } from "../common/AppLogger";
import { PATTERNS } from "../utils/attacks_parttens";
import { MaliciousPatternType, } from "../services/MaliciousPatterns.service";
export class NMPSUtils {
constructor(sanitizeInput) {
NMPSUtils.sanitizeInput = sanitizeInput;
}
static isSafeHighEntropy(input) {
// JWT pattern
if (/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(input)) {
return true;
}
// Data URI for images
if (/^data:image\/(png|jpeg|gif);base64,[A-Za-z0-9+/=]+$/.test(input)) {
return true;
}
// API key pattern (example: 32+ alphanumeric characters)
if (/^[A-Za-z0-9]{32,}$/.test(input)) {
return true;
}
return false;
}
/**
* Generates a recommendation specifically for URLs based on detected patterns
*/
static generateUrlRecommendation(detectedPatterns, componentResults) {
if (detectedPatterns.length === 0) {
return "No suspicious patterns detected in the URL.";
}
const componentIssues = [];
let hasCriticalIssue = false;
// Check severity of issues by component
Object.entries(componentResults).forEach(([component, result]) => {
if (result.detectedPatterns.length > 0) {
const highSeverity = result.detectedPatterns.some((p) => p.severity === "high");
if (highSeverity) {
hasCriticalIssue = true;
componentIssues.push(`Critical issues found in the ${component} component`);
}
else {
componentIssues.push(`Suspicious patterns found in the ${component} component`);
}
}
});
// Generate overall recommendation
if (hasCriticalIssue) {
return `This URL contains potentially malicious patterns. ${componentIssues.join(". ")}. Consider blocking this URL and scanning related systems for compromise.`;
}
else if (componentIssues.length > 1) {
return `This URL has multiple suspicious components: ${componentIssues.join("; ")}. Recommend further review before processing this URL.`;
}
else {
return `This URL contains suspicious patterns. ${componentIssues[0]}. Proceed with caution and validate the URL source.`;
}
}
/**
* Finds related patterns across different components that might indicate a sophisticated attack
*/
static findRelatedPatternGroups(patterns) {
const groups = [];
// Find cross-site scripting patterns across multiple components
const xssPatterns = patterns.filter((p) => p.type === MaliciousPatternType.XSS);
if (xssPatterns.length > 1) {
groups.push({
patterns: [MaliciousPatternType.XSS],
description: "Multiple XSS vectors detected across different URL components",
riskMultiplier: 1.5,
});
}
// Find SQL injection patterns across multiple components
const sqlInjectionPatterns = patterns.filter((p) => p.type === MaliciousPatternType.SQL_INJECTION);
if (sqlInjectionPatterns.length > 1) {
groups.push({
patterns: [MaliciousPatternType.SQL_INJECTION],
description: "Multiple SQL injection vectors detected across different URL components",
riskMultiplier: 1.6,
});
}
// Find encoding obfuscation techniques
const encodingPatterns = patterns.filter((p) => p.type === MaliciousPatternType.ENCODED_PAYLOAD ||
p.type === MaliciousPatternType.MULTI_ENCODING);
// Check for encoding + injection combination (sophisticated attack)
if (encodingPatterns.length > 0) {
const injectionPatterns = patterns.filter((p) => p.type === MaliciousPatternType.SQL_INJECTION ||
p.type === MaliciousPatternType.XSS ||
p.type === MaliciousPatternType.COMMAND_INJECTION ||
p.type === MaliciousPatternType.TEMPLATE_INJECTION ||
p.type === MaliciousPatternType.NOSQL_INJECTION);
if (injectionPatterns.length > 0) {
groups.push({
patterns: [
MaliciousPatternType.ENCODED_PAYLOAD,
...injectionPatterns.map((p) => p.type),
],
description: "Encoded payload combined with injection attempt - sophisticated evasion technique",
riskMultiplier: 2.0,
});
}
}
// Detect potential combination attacks
const hasRedirect = patterns.some((p) => p.type === MaliciousPatternType.OPEN_REDIRECT);
const hasXss = patterns.some((p) => p.type === MaliciousPatternType.XSS);
if (hasRedirect && hasXss) {
groups.push({
patterns: [
MaliciousPatternType.OPEN_REDIRECT,
MaliciousPatternType.XSS,
],
description: "Combined redirect and XSS attack vector detected",
riskMultiplier: 1.8,
});
}
// Detect protocol confusion attacks
const hasProtocolConfusion = patterns.some((p) => p.type === MaliciousPatternType.PROTOCOL_CONFUSION);
const hasSsrf = patterns.some((p) => p.type === MaliciousPatternType.SSRF);
if (hasProtocolConfusion && hasSsrf) {
groups.push({
patterns: [
MaliciousPatternType.PROTOCOL_CONFUSION,
MaliciousPatternType.SSRF,
],
description: "Protocol confusion combined with SSRF attempt",
riskMultiplier: 1.9,
});
}
return groups;
}
/**
* Checks a string against a set of regex patterns for a specific attack type
*/
static checkPatterns(input, patterns, type, description, severity, results, options) {
for (const pattern of patterns) {
const match = pattern.exec(input);
if (match) {
const matchedValue = match[0];
const confidence = this.calculateConfidence(matchedValue, input);
const contextScore = options.enableContextualAnalysis
? this.calculateContextScore(match, input)
: undefined;
results.push({
type,
pattern: pattern.toString(),
location: `index: ${match.index}`,
severity,
confidence,
description,
matchedValue,
contextScore,
});
if (options.debug) {
AppLogger.debug(`NMPS: Detected ${type} pattern: '${matchedValue}' at index ${match.index}`);
}
// Only report the first match for each pattern to avoid redundancy
break;
}
}
}
/**
* Checks for suspicious parameter names in a URL
*/
static checkSuspiciousParameters(input, results, options) {
try {
// Try to extract parameter names from URL query string
let params = [];
// Check if input contains URL parameters
const queryStart = input.indexOf("?");
if (queryStart !== -1) {
const queryString = input.substring(queryStart + 1);
const pairs = queryString.split("&");
for (const pair of pairs) {
const eqIndex = pair.indexOf("=");
if (eqIndex !== -1) {
const paramName = pair.substring(0, eqIndex).toLowerCase();
params.push(paramName);
}
}
}
// Check parameter names against suspicious list
for (const param of params) {
if (this.SUSPICIOUS_PARAMETER_NAMES.includes(param)) {
results.push({
type: MaliciousPatternType.SUSPICIOUS_PARAMETER,
pattern: "suspicious_param_name",
location: `parameter: ${param}`,
severity: "low",
confidence: "medium",
description: `Suspicious parameter name "${param}" detected`,
matchedValue: param,
});
if (options.debug) {
AppLogger.debug(`NMPS: Detected suspicious parameter: '${param}'`);
}
}
}
}
catch (error) {
AppLogger.error("Error in checkSuspiciousParameters:", error);
}
}
/**
* Calculates confidence level for a pattern match based on match characteristics
*/
// In NehonixSecurity.service.txt
static calculateConfidence(matchedValue, fullInput) {
const matchRatio = matchedValue.length / fullInput.length;
// High-confidence patterns
const highConfidencePatterns = [
/<script/i,
/javascript:/i,
/onload=/i,
/alert\(/i,
/select.*from/i,
/union.*select/i,
/whoami/i,
/ping/i,
/etc\/passwd/i,
/localhost/i,
];
if (highConfidencePatterns.some((pattern) => pattern.test(matchedValue))) {
return "high";
}
if (matchRatio < 0.1 && matchedValue.length < 5) {
return "low";
}
if (this.isLikelyFalsePositive(matchedValue, fullInput)) {
return "low";
}
if (matchedValue.length >= 10 || matchRatio > 0.5) {
return "high";
}
return "medium";
}
/**
* Checks if a match is likely a false positive based on context
*/
// In NehonixSecurity.service.txt
static isLikelyFalsePositive(match, fullInput) {
const falsePositiveContexts = [
/https?:\/\/[^\/]+\/documentation\/examples?\/.*sql/i,
/https?:\/\/[^\/]+\/blog\/.*security/i,
/code example/i,
/security training/i,
/\/\/\s*Example/i,
// Ignore common URL parameters
/\?(sort|filter|category|page|limit|offset)=/i,
];
for (const context of falsePositiveContexts) {
if (context.test(fullInput)) {
return true;
}
}
// Ignore matches in legitimate query parameters
if (/[&?]sort=/.test(fullInput) && match.includes("sort")) {
return true;
}
/**
* TODO: Add a feed back for false positives
* */
return false;
}
/**
* Calculates additional context score for pattern matches
*/
static calculateContextScore(match, fullInput) {
let score = 1.0;
const start = Math.max(0, match.index - 10);
const end = Math.min(fullInput.length, match.index + match[0].length + 10);
const context = fullInput.substring(start, end);
if (/\\x|\\u|%u|%[0-9a-f]{2}|&#\d+;|&#x[0-9a-f]+;/i.test(context)) {
score *= 1.5;
}
if (/\/\*|\*\/|--|#|\/{2}/i.test(context)) {
score *= 1.3;
}
const criticalPatterns = [
/etc\/passwd/i,
/localhost/i,
/192\.168\.\d{1,3}\.\d{1,3}/i,
/file=https?:\/\//i,
];
for (const pattern of criticalPatterns) {
if (pattern.test(context)) {
score *= 1.5;
break;
}
}
return score;
}
/**
* Performs contextual analysis on detected patterns to improve detection accuracy
*/
static performContextualAnalysis(patterns, fullInput, options) {
// Find relationships between detected patterns
const relatedGroups = this.findRelatedPatternGroups(patterns);
// Calculate entropy score
const entropyScore = this.calculateEntropy(fullInput);
// Detect potential encoding layers
const encodingLayers = this.detectEncodingLayers(fullInput);
// Calculate anomaly score based on character distribution
const anomalyScore = this.calculateAnomalyScore(fullInput);
return {
relatedPatterns: relatedGroups,
entropyScore,
anomalyScore,
encodingLayers,
};
}
/**
* Calculates Shannon entropy of a string to detect random or encoded content
* Higher entropy often indicates encryption or encoding
*/
static calculateEntropy(input) {
const len = input.length;
const charCounts = {};
// Count character occurrences
for (let i = 0; i < len; i++) {
const char = input[i];
charCounts[char] = (charCounts[char] || 0) + 1;
}
// Calculate entropy
let entropy = 0;
for (const char in charCounts) {
const probability = charCounts[char] / len;
entropy -= probability * Math.log2(probability);
}
return entropy;
}
/**
* Detects number of potential encoding layers in a string
*/
static detectEncodingLayers(input) {
let layers = 0;
let currentInput = input;
const maxLayers = 5; // Prevent infinite loops
let attempts = 0;
while (attempts < maxLayers) {
let decoded = currentInput;
// URL decoding
if (/%[0-9A-Fa-f]{2}/.test(decoded)) {
try {
decoded = decodeURIComponent(decoded);
if (decoded !== currentInput) {
layers++;
currentInput = decoded;
}
}
catch { }
}
// HTML entities
if (/&[#a-zA-Z0-9]+;/.test(decoded)) {
const temp = decoded
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/&/g, "&")
.replace(/"/g, '"')
.replace(/&#x[0-9a-fA-F]+;/g, (match) => {
try {
const hex = match.substring(3, match.length - 1);
return String.fromCodePoint(parseInt(hex, 16));
}
catch {
return match;
}
})
.replace(/&#\d+;/g, (match) => {
try {
const decimal = match.substring(2, match.length - 1);
return String.fromCodePoint(parseInt(decimal, 10));
}
catch {
return match;
}
});
if (temp !== decoded) {
layers++;
currentInput = temp;
}
}
// Unicode escapes
if (/\\u[0-9a-fA-F]{4}/.test(decoded)) {
try {
const temp = decoded.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => {
return String.fromCodePoint(parseInt(hex, 16));
});
if (temp !== decoded) {
layers++;
currentInput = temp;
}
}
catch { }
}
// Base64
if (/^[A-Za-z0-9+/=]+$/.test(decoded)) {
try {
const temp = Buffer.from(decoded, "base64").toString();
if (temp !== decoded && temp.length > 0) {
layers++;
currentInput = temp;
}
}
catch { }
}
// If no further decoding occurred, break
if (decoded === currentInput) {
break;
}
attempts++;
}
return layers;
}
/**
* Calculates statistical anomaly score based on character distribution
*/
static calculateAnomalyScore(input) {
const len = input.length;
if (len === 0)
return 0;
// Character type counts
let lowerCount = 0;
let upperCount = 0;
let digitCount = 0;
let specialCount = 0;
let controlCount = 0;
let nonAsciiCount = 0;
// Count character types
for (let i = 0; i < len; i++) {
const code = input.charCodeAt(i);
if (code >= 97 && code <= 122) {
// a-z
lowerCount++;
}
else if (code >= 65 && code <= 90) {
// A-Z
upperCount++;
}
else if (code >= 48 && code <= 57) {
// 0-9
digitCount++;
}
else if (code < 32 || code === 127) {
// Control characters
controlCount++;
}
else if (code > 127) {
// Non-ASCII
nonAsciiCount++;
}
else {
specialCount++; // Other special characters
}
}
// Calculate percentages
const lowerPercent = lowerCount / len;
const upperPercent = upperCount / len;
const digitPercent = digitCount / len;
const specialPercent = specialCount / len;
const controlPercent = controlCount / len;
const nonAsciiPercent = nonAsciiCount / len;
// Calculate anomaly score - higher means more unusual distribution
let anomalyScore = 0;
// Unusual amount of special characters
if (specialPercent > 0.3) {
anomalyScore += specialPercent * 2;
}
// Unusual amount of non-ASCII characters
if (nonAsciiPercent > 0.1) {
anomalyScore += nonAsciiPercent * 3;
}
// Presence of control characters is very suspicious
if (controlPercent > 0) {
anomalyScore += controlPercent * 5;
}
// Unusual character type distribution
const alphaPercent = lowerPercent + upperPercent;
if (alphaPercent < 0.2 && len > 10) {
anomalyScore += 1;
}
// Very high percentage of digits can be suspicious
if (digitPercent > 0.7 && len > 10) {
anomalyScore += 0.5;
}
return Math.min(anomalyScore, 5); // Cap at 5
}
/**
* Calculates total risk score based on detected patterns
*/
// In NehonixSecurity.service.txt
static calculateTotalScore(patterns, sensitivityMultiplier) {
if (patterns.length === 0)
return 0;
let score = 0;
const severityScores = {
high: 40,
medium: 20,
low: 10,
};
const criticalPatternMultipliers = {
[MaliciousPatternType.PATH_TRAVERSAL]: 1.5,
[MaliciousPatternType.SSRF]: 1.5,
[MaliciousPatternType.RFI]: 1.5,
};
const confidenceMultipliers = {
high: 1.5,
medium: 1.0,
low: 0.5,
};
for (const pattern of patterns) {
let patternScore = severityScores[pattern.severity] *
confidenceMultipliers[pattern.confidence];
// Apply critical pattern multiplier
if (criticalPatternMultipliers[pattern.type]) {
patternScore *=
criticalPatternMultipliers[pattern.type];
}
if (pattern.contextScore) {
patternScore *= pattern.contextScore;
}
score += patternScore;
}
score *= sensitivityMultiplier;
const patternTypeCounts = {};
for (const pattern of patterns) {
patternTypeCounts[pattern.type] =
(patternTypeCounts[pattern.type] || 0) + 1;
}
for (const type in patternTypeCounts) {
if (patternTypeCounts[type] > 1) {
score *= 1 + (patternTypeCounts[type] - 1) * 0.2;
}
}
return Math.min(Math.round(score), 100);
}
/**
* Determines overall confidence level based on score and pattern count
*/
static determineConfidence(score, patternCount) {
if (score >= 75 || (score >= 50 && patternCount >= 3)) {
return "high";
}
else if (score >= 40 || (score >= 25 && patternCount >= 2)) {
return "medium";
}
else {
return "low";
}
}
/**
* Generates appropriate recommendation based on detected patterns
*/
static generateRecommendation(patterns, score) {
if (patterns.length === 0) {
return "No malicious patterns detected. Input appears safe.";
}
const patternTypes = new Set(patterns.map((p) => p.type));
const recommendations = [];
// Critical recommendations first
if (score >= 75) {
recommendations.push("HIGH RISK: Input contains malicious patterns. Block and investigate immediately.");
}
else if (score >= 50) {
recommendations.push("MEDIUM RISK: Input contains suspicious patterns. Validate before processing.");
}
else {
recommendations.push("LOW RISK: Input contains potentially suspicious patterns. Use caution.");
}
// Specific recommendations based on pattern types
if (patternTypes.has(MaliciousPatternType.SQL_INJECTION)) {
recommendations.push("Implement prepared statements or parameterized queries for database operations.");
}
if (patternTypes.has(MaliciousPatternType.XSS)) {
recommendations.push("Implement output encoding and content security policy (CSP) headers.");
}
if (patternTypes.has(MaliciousPatternType.COMMAND_INJECTION)) {
recommendations.push("Avoid direct command execution. Use restricted APIs and allowlists.");
}
if (patternTypes.has(MaliciousPatternType.PATH_TRAVERSAL)) {
recommendations.push("Validate file paths and use path canonicalization before file operations.");
}
if (patternTypes.has(MaliciousPatternType.SSRF)) {
recommendations.push("Implement allowlists for external resource access and validate URLs.");
}
if (patternTypes.has(MaliciousPatternType.RFI)) {
recommendations.push("Validate file inclusions against a whitelist of allowed sources and disable remote file access.");
}
if (patternTypes.has(MaliciousPatternType.ENCODED_PAYLOAD) ||
patternTypes.has(MaliciousPatternType.MULTI_ENCODING)) {
recommendations.push("Decode and normalize input before validation to prevent evasion techniques.");
}
return recommendations.join(" ");
}
/**
* Checks if content is likely HTML/JavaScript code fragment
*
* @param input - String to check
* @returns boolean indicating if it looks like code
*/
static isLikelyCode(input) {
// Check for common code patterns
const codePatterns = [
/<\w+>[\s\S]*<\/\w+>/i, // HTML tags
/<\w+\s+[\w\-]+=(['"]).*?\1(\s+[\w\-]+=(['"]).*?\3)*\s*>/i, // HTML tags with attributes
/function\s*\([^)]*\)\s*\{/i, // JavaScript function declaration
/var\s+\w+\s*=|let\s+\w+\s*=|const\s+\w+\s*=/i, // Variable declarations
/if\s*\([^)]*\)\s*\{/i, // if statement
/for\s*\([^)]*\)\s*\{/i, // for loop
/\bclass\s+\w+\s*\{/i, // class declaration
/import\s+.*\s+from\s+['"].*['"]/i, // ES6 imports
/export\s+(default\s+)?(function|class|const)/i, // ES6 exports
];
return codePatterns.some((pattern) => pattern.test(input));
}
/**
* Checks if a tag is in the allowed safe tags list
* @param tag - The tag name to check
* @returns Whether the tag is considered safe
*/
static isSafeTag(tag) {
const safeTags = [
"p",
"br",
"b",
"i",
"em",
"strong",
"small",
"ul",
"ol",
"li",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"span",
"div",
"table",
"tr",
"td",
"th",
"thead",
"tbody",
];
return safeTags.includes(tag.toLowerCase());
}
/**
* Creates a placeholder of specified length to preserve original string length
* @param type - Type of content being replaced
* @param length - Length of the original content
* @returns Placeholder string of approximately the same length
*/
static createPlaceholder(type, length) {
if (length <= 0)
return "";
return `[${type}_${Array(length)
.fill("x")
.join("")
.substring(0, length - type.length - 2)}]`;
}
/**
* Checks if input appears to be a URL
* @param input - String to check
* @returns Whether the input looks like a URL
*/
static isUrlLike(input) {
return (/^(https?:\/\/|www\.|\/\/)/i.test(input.trim()) ||
/\.(com|org|net|io|gov|edu)($|\/|\?)/i.test(input));
}
/**
* Specialized sanitization for URLs
* @param url - URL to sanitize
* @param opts - Sanitization options
* @returns Sanitized URL
*/
sanitizeUrl(url, opts) {
// Block potentially dangerous protocols
const dangerousProtocols = [
"javascript:",
"data:",
"vbscript:",
"file:",
"about:",
];
for (const protocol of dangerousProtocols) {
if (url.toLowerCase().includes(protocol)) {
return opts.strictMode
? ""
: url.replace(new RegExp(protocol, "gi"), `${protocol.replace(":", "_blocked:")}`);
}
}
// Ensure URL structure is valid
try {
// Check if URL needs a protocol to be parsed
if (!url.match(/^[a-z]+:\/\//i)) {
// Add protocol for validation purposes only
url = "https://" + url.replace(/^\/\//, "");
}
const parsedUrl = new URL(url);
// Check for potential open redirects
if (parsedUrl.searchParams.toString().match(/url=|redirect=|to=/i)) {
return this.handlePotentialRedirect(url, opts);
}
return url;
}
catch (e) {
// If URL is malformed, return it with a warning flag if not in strict mode
return opts.strictMode ? "" : `[potential_malformed_url]${url}`;
}
}
/**
* Handle potential redirect parameters in URLs
* @param url - URL to check
* @param opts - Sanitization options
* @returns Sanitized URL
*/
handlePotentialRedirect(url, opts) {
try {
const parsedUrl = new URL(url);
// List of common redirect parameter names
const redirectParams = [
"url",
"redirect",
"to",
"target",
"link",
"goto",
];
// Check each parameter
for (const param of redirectParams) {
if (parsedUrl.searchParams.has(param)) {
const redirectValue = parsedUrl.searchParams.get(param);
// If redirect value is present, sanitize it and update the parameter
if (redirectValue) {
parsedUrl.searchParams.set(param, NMPSUtils.sanitizeInput(redirectValue, {
...opts,
strictMode: true, // More strict for embedded URLs
}));
}
}
}
return parsedUrl.toString();
}
catch (e) {
// If there's an error processing the URL, return safe version
return opts.strictMode ? "" : `[redirect_sanitized]${url}`;
}
}
}
NMPSUtils.SUSPICIOUS_PARAMETER_NAMES = PATTERNS.SUSPICIOUS_PARAMETER_NAMES;
//# sourceMappingURL=NMPSUtils.js.map