""" dependency_scanner.py — JavaScript / Backend Dependency Vulnerability Scanner ============================================================================== Detects outdated and vulnerable client-side libraries by: 1. Parsing package.json / composer.json / requirements.txt if exposed 2. Fingerprinting loaded JS libraries from HTML source (jQuery, React, etc.) 3. Comparing detected versions against a built-in known-vulnerable version database 4. Detecting missing Subresource Integrity (SRI) on external scripts """ import re, json, urllib.request, urllib.error from packaging import version as pkg_version from scanners.base_scanner import BaseScanner # ── Known-vulnerable version ranges ──────────────────────────────────────── # Format: library_name -> [(max_vulnerable_version, CVE, severity, cvss, description)] VULN_DB = { "jquery": [ ("1.12.4", "CVE-2015-9251", "Medium", 6.1, "XSS via jQuery.htmlPrefilter"), ("3.4.9", "CVE-2019-11358", "Medium", 6.1, "Prototype pollution in jQuery.extend"), ("3.6.0", "CVE-2020-23064", "Medium", 6.9, "XSS via HTML parsing"), ], "bootstrap": [ ("3.4.1", "CVE-2019-8331", "Medium", 6.1, "XSS in tooltip/popover data-template"), ("4.3.0", "CVE-2018-14042", "Medium", 6.1, "XSS via data-target attribute"), ], "angularjs": [ ("1.8.3", "CVE-2022-25869", "Medium", 6.1, "XSS in $sanitize"), ("1.6.9", "CVE-2019-14863", "Medium", 6.1, "Angular template injection"), ], "lodash": [ ("4.17.20", "CVE-2021-23337", "High", 7.2, "Command injection via template"), ("4.17.15", "CVE-2020-28500", "Medium", 5.3, "ReDoS in toNumber"), ("4.17.10", "CVE-2019-10744", "Critical", 9.1, "Prototype pollution"), ], "moment": [ ("2.29.3", "CVE-2022-24785", "High", 7.5, "Path traversal in locale loading"), ("2.29.1", "CVE-2022-31129", "High", 7.5, "ReDoS in string-to-date parsing"), ], "axios": [ ("0.21.1", "CVE-2021-3749", "High", 7.5, "Server-side request forgery via SSRF bypass"), ], "d3": [ ("5.16.0", "CVE-2021-23490", "Medium", 5.3, "Prototype pollution"), ], "react": [ ("16.13.0", "CVE-2018-6341", "Medium", 6.1, "XSS via dangerouslySetInnerHTML"), ], "vue": [ ("2.6.14", "CVE-2022-23912", "Medium", 5.3, "ReDoS"), ], } # JS fingerprint patterns: (regex in src/content, library_name, version_capture_group) JS_FINGERPRINTS = [ (re.compile(r'jquery[.-]?(\d+\.\d+\.\d+)', re.I), "jquery"), (re.compile(r'bootstrap[.-]?(\d+\.\d+\.\d+)', re.I), "bootstrap"), (re.compile(r'angular[.-]?(\d+\.\d+\.\d+)', re.I), "angularjs"), (re.compile(r'lodash[.-]?(\d+\.\d+\.\d+)', re.I), "lodash"), (re.compile(r'moment[.-]?(\d+\.\d+\.\d+)', re.I), "moment"), (re.compile(r'axios[.-]?(\d+\.\d+\.\d+)', re.I), "axios"), (re.compile(r'react[.-]?(\d+\.\d+\.\d+)', re.I), "react"), (re.compile(r'vue[.-]?(\d+\.\d+\.\d+)', re.I), "vue"), (re.compile(r'd3[.-]?v?(\d+\.\d+\.\d+)', re.I), "d3"), ] # Inline version variables: jQuery.fn.jquery, $.fn.jquery, etc. INLINE_VER_RE = { "jquery": re.compile(r'jQuery\.fn\.jquery\s*=\s*["\'](\d+\.\d+\.\d+)["\']'), "react": re.compile(r'ReactDOM\.version\s*=\s*["\'](\d+\.\d+\.\d+)["\']'), "vue": re.compile(r'Vue\.version\s*=\s*["\'](\d+\.\d+\.\d+)["\']'), } EXPOSED_MANIFESTS = [ ("/package.json", "json", "node"), ("/composer.json", "json", "php"), ("/requirements.txt", "text", "python"), ("/Gemfile.lock", "text", "ruby"), ("/go.sum", "text", "go"), ] class DependencyScanner(BaseScanner): SCANNER_NAME = "Dependency Vulnerability Scanner" _SCANNER_KEY = "dependency" def __init__(self, scan_id, target, domain, **kwargs): super().__init__(scan_id, target, domain, **kwargs) self._detected: dict = {} # library -> version self._reported: set = set() # ------------------------------------------------------------------ def run(self) -> list: self.log("INFO", f"[Deps] Starting dependency vulnerability scan on {self.target}...") try: html, script_urls = self._fetch_page() self._fingerprint_from_html(html, script_urls) self._check_manifest_exposure() self._check_sri(html) self._audit_detected() except Exception as e: self.log("WARNING", f"[Deps] Error: {e}") self.log( "SUCCESS" if not self.vulns else "WARNING", f"[Deps] Complete. Detected {len(self._detected)} lib(s). " f"{len(self.vulns)} issue(s) found.", ) return self.vulns # ------------------------------------------------------------------ def _fetch_page(self) -> tuple: req = urllib.request.Request(self.target, headers={"User-Agent": "LarShield/2.0 Dep-Scanner"}) with urllib.request.urlopen(req, timeout=8, context=self.get_ssl_context()) as r: html = r.read().decode("utf-8", errors="ignore") scripts = re.findall(r']+src=["\']([^"\']+)["\']', html, re.I) return html, scripts # ------------------------------------------------------------------ def _fingerprint_from_html(self, html: str, script_urls: list): # Check script src URLs all_text = html + "\n" + "\n".join(script_urls) for pat, lib in JS_FINGERPRINTS: m = pat.search(all_text) if m: ver = m.group(1) if lib not in self._detected: self._detected[lib] = ver self.log("INFO", f"[Deps] Detected: {lib} v{ver}") # Check inline version variables in JS content for script_url in script_urls[:8]: try: full_url = self._resolve_url(script_url) req = urllib.request.Request(full_url, headers={"User-Agent": "LarShield/2.0 Dep-Scanner"}) with urllib.request.urlopen(req, timeout=5, context=self.get_ssl_context()) as r: js = r.read().decode("utf-8", errors="ignore") for lib, ver_re in INLINE_VER_RE.items(): vm = ver_re.search(js) if vm and lib not in self._detected: self._detected[lib] = vm.group(1) self.log("INFO", f"[Deps] Detected (inline): {lib} v{vm.group(1)}") except Exception as e: self.log("ERROR", f"[Deps] Inline detection error: {e}") continue # ------------------------------------------------------------------ def _check_manifest_exposure(self): base = self.target.rstrip("/") for path, fmt, ecosystem in EXPOSED_MANIFESTS: url = f"{base}{path}" body, status = self._probe(url) if status == 200 and body: self.add_vuln( title=f"Exposed Dependency Manifest: {path}", severity="Medium", category="Information Disclosure", cvss_score=5.3, description=f"The {ecosystem} dependency manifest `{url}` is publicly " "accessible. This reveals the exact library versions in use, " "allowing targeted exploitation of known CVEs.", remediation=f"Block access to `{path}` in your web server config:\n" "Nginx: location = /package.json { deny all; return 404; }", ) # Also parse versions from manifest if fmt == "json": try: data = json.loads(body) deps = {**data.get("dependencies",{}), **data.get("devDependencies",{})} for lib, ver_str in deps.items(): clean_ver = ver_str.lstrip("^~>=<") lib_key = lib.lower().split("/")[-1] if lib_key in VULN_DB and lib_key not in self._detected: self._detected[lib_key] = clean_ver except Exception as e: self.log("ERROR", f"[Deps] Manifest JSON parse error: {e}") # ------------------------------------------------------------------ def _check_sri(self, html: str): ext_scripts = re.findall( r']+src=["\']https?://[^"\']+["\'][^>]*>', html, re.I) missing_sri = [s for s in ext_scripts if "integrity=" not in s.lower()] if missing_sri: self.add_vuln( title=f"Subresource Integrity (SRI) Missing on {len(missing_sri)} External Script(s)", severity="Medium", category="Supply Chain Security", cvss_score=5.9, description=f"Found {len(missing_sri)} external `\n' "Generate SRI hashes at: https://www.srihash.org/", ) else: self.log("SUCCESS", "[Deps] All external scripts have SRI attributes") # ------------------------------------------------------------------ def _audit_detected(self): for lib, detected_ver in self._detected.items(): if lib not in VULN_DB: continue try: dv = pkg_version.parse(detected_ver) except Exception as e: self.log("ERROR", f"[Deps] Version parse error for {lib}: {e}") continue for max_vuln_ver, cve, severity, cvss, desc in VULN_DB[lib]: try: mv = pkg_version.parse(max_vuln_ver) except Exception as e: self.log("ERROR", f"[Deps] Version parse error for {max_vuln_ver}: {e}") continue key = f"{lib}:{cve}" if dv <= mv and key not in self._reported: self._reported.add(key) self.log("WARNING", f"[Deps] VULNERABLE: {lib} v{detected_ver} <= v{max_vuln_ver} ({cve})") self.add_vuln( title=f"Vulnerable Library: {lib} v{detected_ver} ({cve})", severity=severity, category="Vulnerable Dependencies", cvss_score=cvss, description=f"The detected version of **{lib}** (`v{detected_ver}`) " f"is at or below the vulnerable version `v{max_vuln_ver}`.\n\n" f"**CVE:** {cve}\n**Impact:** {desc}\n\n" "An attacker can exploit this known vulnerability via client-side attacks.", remediation=f"Update {lib} to the latest stable version.\n" f"See: https://nvd.nist.gov/vuln/detail/{cve}", ) # ------------------------------------------------------------------ def _probe(self, url: str) -> tuple: try: req = urllib.request.Request(url, headers={"User-Agent": "LarShield/2.0 Dep-Scanner"}) with urllib.request.urlopen(req, timeout=5, context=self.get_ssl_context()) as r: return r.read().decode("utf-8", errors="ignore"), r.status except urllib.error.HTTPError as e: return "", e.code except Exception as e: self.log("ERROR", f"[Deps] Probe error: {e}") return None, 0 def _resolve_url(self, src: str) -> str: if src.startswith("//"): return f"https:{src}" if src.startswith("/"): return f"{self.target.rstrip('/')}{src}" if not src.startswith("http"): return f"{self.target.rstrip('/')}/{src}" return src