@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
120 lines • 4.88 kB
JavaScript
// Import Third-party Dependencies
import ipaddress from "ipaddr.js";
// Import Internal Dependencies
import { toArrayLocation } from "./utils/toArrayLocation.js";
import { CollectableSetRegistry } from "./CollectableSetRegistry.js";
// CONSTANTS
const kShadyLinkRegExps = [
/(http[s]?:\/\/(bit\.ly|ipinfo\.io|httpbin\.org|api\.ipify\.org).*)$/,
/(http[s]?:\/\/.*\.(link|xyz|tk|ml|ga|cf|gq|pw|top|club|mw|bd|ke|am|sbs|date|quest|cd|bid|ws|icu|cam|uno|email|stream))$/
];
// List of known URI schemes (IANA registered + common ones)
// See: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
const kKnownProtocols = new Set([
// Web
"http:", "https:",
// File & Data
"file:", "data:", "blob:",
// FTP
"ftp:", "ftps:", "sftp:", "tftp:",
// Mail & Messaging
"mailto:", "xmpp:", "irc:", "ircs:", "sip:", "sips:", "tel:", "sms:", "mms:",
// Remote access
"ssh:", "telnet:", "vnc:", "rdp:",
// Version control
"git:", "svn:", "cvs:", "hg:",
// P2P & Torrents
"magnet:", "ed2k:", "torrent:",
// Crypto & Blockchain
"bitcoin:", "ethereum:", "ipfs:", "ipns:",
// App-specific
"slack:", "discord:", "spotify:", "steam:", "skype:", "zoommtg:", "msteams:",
"vscode:", "vscode-insiders:", "jetbrains:",
// Mobile & Desktop deep links
"intent:", "market:", "itms:", "itms-apps:", "fb:", "twitter:", "instagram:", "whatsapp:", "tg:",
// Other common protocols
"ws:", "wss:", "ldap:", "ldaps:", "nntp:", "news:", "rtsp:", "rtspu:", "rtsps:",
"webcal:", "feed:", "podcast:",
// eslint-disable-next-line no-script-url
"javascript:", "about:", "view-source:",
// Security related
"acap:", "cap:", "cid:", "mid:", "urn:", "tag:", "dns:", "geo:", "ni:", "nih:"
]);
export class ShadyLink {
static isURLSafe(input, options) {
if (!URL.canParse(input)) {
return { safe: true };
}
const parsedUrl = new URL(input);
// Unknown protocol, not a real URL
if (!kKnownProtocols.has(parsedUrl.protocol)) {
return { safe: true };
}
const { collectableSetRegistry, file, location, metadata } = options;
const sourceArrayLocation = toArrayLocation(location);
collectableSetRegistry.add("url", { value: parsedUrl.href, file, location: sourceArrayLocation, metadata });
const hostname = parsedUrl.hostname;
// Early check for localhost
if (hostname === "localhost") {
collectableSetRegistry.add("hostname", { value: hostname, file, location: sourceArrayLocation, metadata });
return { safe: false, isLocalAddress: true };
}
if (parsedUrl.protocol === "file:") {
if (hostname) {
collectableSetRegistry.add("hostname", { value: hostname, file, location: sourceArrayLocation, metadata });
}
return { safe: true };
}
// Remove brackets from IPv6 addresses (e.g., "[::1]" -> "::1")
const cleanHostname = hostname.startsWith("[") && hostname.endsWith("]")
? hostname.slice(1, -1)
: hostname;
if (this.isValidIPAddress(cleanHostname)) {
const result = this.isIpAddressSafe(cleanHostname, {
collectableSetRegistry,
file,
location: sourceArrayLocation,
metadata
});
if (!result.safe) {
return result;
}
}
else if (hostname) {
collectableSetRegistry.add("hostname", { value: hostname, file, location: sourceArrayLocation, metadata });
}
const scheme = parsedUrl.protocol.replace(":", "");
if (scheme !== "https") {
return { safe: false };
}
const isShadyLink = kShadyLinkRegExps.some((regex) => regex.test(input));
return { safe: !isShadyLink };
}
static isValidIPAddress(input) {
return /\D/.test(input) && ipaddress.isValid(input);
}
static isIpAddressSafe(input, options) {
const { collectableSetRegistry, file, location, metadata } = options;
collectableSetRegistry.add("ip", {
value: input, file,
location: Array.isArray(location) ? location : toArrayLocation(location),
metadata
});
if (this.#isPrivateIPAddress(input)) {
return { safe: false, isLocalAddress: true };
}
return { safe: true };
}
static #isPrivateIPAddress(ipAddress) {
let ip = ipaddress.parse(ipAddress);
if (ip instanceof ipaddress.IPv6 && ip.isIPv4MappedAddress()) {
ip = ip.toIPv4Address();
}
const range = ip.range();
if (range !== "unicast") {
return true;
}
return false;
}
}
//# sourceMappingURL=ShadyLink.js.map