File size: 12,816 Bytes
d543fc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
"""

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'<script[^>]+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'<script[^>]+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 `<script>` tag(s) without "
                    "`integrity=` attributes. Without SRI, if the CDN is compromised or the "
                    "file is modified, malicious code executes in users' browsers silently.\n\n"
                    f"Example: `{missing_sri[0][:200]}`",
                remediation="Add integrity and crossorigin attributes to all external scripts:\n"
                    '<script src="https://cdn.example.com/lib.js" '
                    'integrity="sha384-..." crossorigin="anonymous"></script>\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