Skip to Content
OpenVAPTAlpha
Pre-release. Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. Free for your own use, 100% at your own risk. This website collects no data. Read MoreRead Less
Pre-release. OpenVAPT has no Stable release yet. Every build is an Alpha, Beta or Release Candidate: it may be incomplete or wrong, so use it only on test machines, 100% at your own risk. See Release Channels
Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. These are AI systems, not people or registered engineers. Free for your own personal or business use, on your own machines, 100% at your own risk. Not for sale. Not for providing services. This website collects no data. Read the Acknowledgements
Pre-release. OpenVAPT has no Stable release yet. Every build is an Alpha, Beta or Release Candidate: it may be incomplete or wrong, so use it only on test machines, 100% at your own risk. See Release Channels
Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. These are AI systems, not people or registered engineers. Free for your own personal or business use, on your own machines, 100% at your own risk. Not for sale. Not for providing services. This website collects no data. Read the Acknowledgements
LowWEB-EXP-009Information ExposureWeb Apps

Dependency Manifests Exposed

Checks whether package manifests and lock files are downloadable.

Why It Matters
Lock files list exact library versions, giving attackers a ready list to match against public vulnerabilities, and often reveal internal package names.
How It Detects
Requests /composer.json, /composer.lock, /package.json, /package-lock.json, /yarn.lock and /Gemfile.lock; a non-HTML 200 response with the file's characteristic structure confirms exposure.
How to Fix
Keep manifests outside the public web root (for example serve only a public/ subfolder), or deny them at the web server.
How to Verify
curl -s -o /dev/null -w '%{http_code}' https://<site>/composer.lock should print 403 or 404.
CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N
Control Category
Information Exposure: CIS Controls v8 3, 16; NIST CSF 2.0 PR.DS
Fix Effort
Low
CIS Controls v8 Safeguard
3.3
NIST CSF 2.0 Subcategory
PR.AA-05, PR.DS-01
OWASP WSTG v4.2
WSTG-CONF-04
CWE
CWE-538
OWASP Top 10
A05:2021, A06:2021
OWASP ASVS 4.0.3
12.5.1
NIST SP 800-53 Rev. 5
CM-7
References
OWASP WSTG-CONF-04, Review Old Backup and Unreferenced Files for Sensitive Information
Learn the theory behind this check: P6 Information Exposure and Misconfiguration.

Source Code

The complete file that defines this check. Run it with openvapt scan --only WEB-EXP-009, or read it offline with openvapt explain WEB-EXP-009.

internal/checks/web/exposure.go · Lines 1-164View on GitHub at This Commit
1package web
2
3import (
4 "context"
5 "strings"
6
7 "github.com/openvapt/openvapt/internal/check"
8 "github.com/openvapt/openvapt/internal/checks/collect"
9 "github.com/openvapt/openvapt/internal/sys"
10)
11
12// probe describes a fixed path to request and how to recognise a real hit.
13// Requiring a content marker avoids false positives from sites that answer
14// 200 with an HTML page for every path ("soft 404").
15type probe struct {
16 path string
17 marker func(sys.HTTPResponse) bool
18}
19
20func bodyHas(subs ...string) func(sys.HTTPResponse) bool {
21 return func(r sys.HTTPResponse) bool {
22 if r.Status != 200 {
23 return false
24 }
25 for _, s := range subs {
26 if strings.Contains(r.Body, s) {
27 return true
28 }
29 }
30 return false
31 }
32}
33
34// binaryHit: a 200 response that is not an HTML page, for archive and dump files.
35func binaryHit(r sys.HTTPResponse) bool { return r.Status == 200 && !HTML(r) && len(r.Body) > 0 }
36
37type exposureRule struct {
38 id, title, summary, rationale, detection, fix, verify string
39 sev check.Severity
40 cat check.Category // empty for legacy rows (set in check.legacy)
41 effort check.Effort
42 cvss string
43 refs []check.Ref
44 std check.Standards
45 probes []probe
46}
47
48var exposureRules = []exposureRule{
49 {
50 id: "WEB-EXP-001", title: "Git Repository Exposed on the Website", sev: check.High,
51 cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
52 summary: "Checks whether the .git folder can be downloaded from each site.",
53 rationale: "An exposed .git folder lets anyone rebuild the full source code and history, often including passwords, API keys and database credentials committed by mistake.",
54 detection: "Requests /.git/HEAD and /.git/config. A 200 response containing 'ref:' or '[core]' confirms exposure.",
55 fix: "Block dot-folders in the web server (Apache: RedirectMatch 404 /\\.git ; nginx: location ~ /\\.(?!well-known) { deny all; }) and deploy without the .git folder in the web root. Rotate any secret that was ever committed.",
56 verify: "curl -s -o /dev/null -w '%{http_code}' https://<site>/.git/HEAD should print 403 or 404.",
57 refs: []check.Ref{{Title: "OWASP WSTG, Review Old Backup and Unreferenced Files (WSTG-CONF-04)", URL: "https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/04-Review_Old_Backup_and_Unreferenced_Files_for_Sensitive_Information/"}},
58 std: check.Standards{CWE: []string{"CWE-527", "CWE-538"}, OWASP: []string{"A05:2021"}, ASVS: []string{"12.5.1"}, WSTG: []string{"WSTG-CONF-04"}, NIST: []string{"CM-7", "SC-28"}},
59 probes: []probe{{"/.git/HEAD", bodyHas("ref:")}, {"/.git/config", bodyHas("[core]")}},
60 },
61 {
62 id: "WEB-EXP-002", title: "Environment or Secrets File Exposed", sev: check.Critical,
63 cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
64 summary: "Checks whether .env and similar configuration files can be downloaded.",
65 rationale: "Framework .env files hold database passwords, mail credentials, encryption keys and API tokens. Downloading one usually means full compromise of the application and its data.",
66 detection: "Requests /.env, /.env.production, /config.php.bak and /wp-config.php.bak. A non-HTML 200 response containing typical key=value secrets (DB_, APP_KEY, DB_PASSWORD) confirms exposure. Contents are redacted in the report.",
67 fix: "Move secrets out of the web root, block dot-files at the web server, delete backup copies, then rotate every secret in the file because it must be treated as leaked.",
68 verify: "curl -s -o /dev/null -w '%{http_code}' https://<site>/.env should print 403 or 404.",
69 refs: []check.Ref{{Title: "OWASP Secrets Management Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"}},
70 std: check.Standards{CWE: []string{"CWE-538", "CWE-260"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.3.2", "2.10.4"}, WSTG: []string{"WSTG-CONF-04"}, NIST: []string{"CM-7", "SC-28"}},
71 probes: []probe{{"/.env", bodyHas("DB_", "APP_KEY", "PASSWORD=")}, {"/.env.production", bodyHas("DB_", "APP_KEY", "PASSWORD=")},
72 {"/wp-config.php.bak", bodyHas("DB_PASSWORD")}, {"/config.php.bak", bodyHas("password", "PASSWORD")}},
73 },
74 {
75 id: "WEB-EXP-003", title: "PHP Information Page Exposed", sev: check.Medium,
76 cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
77 summary: "Checks for phpinfo() pages left on the site.",
78 rationale: "phpinfo() reveals PHP and module versions, file paths, environment variables and sometimes credentials, giving attackers a detailed map of the server.",
79 detection: "Requests /phpinfo.php, /info.php, /php_info.php and /test.php; a 200 response containing the 'PHP Version' heading of phpinfo() confirms it.",
80 fix: "Delete the file. Never leave diagnostic pages on a production server.",
81 verify: "curl -s -o /dev/null -w '%{http_code}' https://<site>/phpinfo.php should print 404.",
82 refs: []check.Ref{{Title: "PHP manual, phpinfo()", URL: "https://www.php.net/manual/en/function.phpinfo.php"}},
83 std: check.Standards{CWE: []string{"CWE-497"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.3.2"}, NIST: []string{"CM-7", "SI-11"}},
84 probes: []probe{{"/phpinfo.php", bodyHas("<title>PHP ", "phpinfo()")}, {"/info.php", bodyHas("<title>PHP ", "phpinfo()")},
85 {"/php_info.php", bodyHas("<title>PHP ", "phpinfo()")}, {"/test.php", bodyHas("<title>PHP ", "phpinfo()")}},
86 },
87 {
88 id: "WEB-EXP-004", title: "Backup or Database Dump Files Downloadable", sev: check.High,
89 cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
90 summary: "Checks for common backup archives and SQL dumps left in the web root.",
91 rationale: "Backups contain the whole site and often the whole database, including customer data and password hashes.",
92 detection: "Requests a short fixed list of common names (backup.zip, backup.tar.gz, site.zip, db.sql, dump.sql, database.sql). A 200 response that is not an HTML page is reported. Only the first 64 KiB are read.",
93 fix: "Move backups outside the web root (for example /home/<user>/deploy-backups/ with mode 700), delete the exposed copies and treat their contents as leaked.",
94 verify: "curl -s -o /dev/null -w '%{http_code}' https://<site>/backup.zip should print 404.",
95 refs: []check.Ref{{Title: "OWASP WSTG-CONF-04, Review Old Backup and Unreferenced Files", URL: "https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/04-Review_Old_Backup_and_Unreferenced_Files_for_Sensitive_Information/"}},
96 std: check.Standards{CWE: []string{"CWE-530"}, OWASP: []string{"A05:2021"}, ASVS: []string{"12.5.1"}, WSTG: []string{"WSTG-CONF-04"}, NIST: []string{"CM-7", "SC-28"}},
97 probes: []probe{{"/backup.zip", binaryHit}, {"/backup.tar.gz", binaryHit}, {"/site.zip", binaryHit},
98 {"/db.sql", bodyHas("CREATE TABLE", "INSERT INTO")}, {"/dump.sql", bodyHas("CREATE TABLE", "INSERT INTO")}, {"/database.sql", bodyHas("CREATE TABLE", "INSERT INTO")}},
99 },
100 {
101 id: "WEB-EXP-005", title: "Directory Listing Enabled", sev: check.Medium,
102 cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
103 summary: "Checks whether the web server lists folder contents when there is no index page.",
104 rationale: "Directory listings reveal every file in a folder, including backups, uploads and scripts never meant to be linked.",
105 detection: "Requests /, /uploads/, /images/, /files/, /backup/ and /wp-content/uploads/ and looks for the auto-index page markers of Apache ('Index of /'), nginx and LiteSpeed.",
106 fix: "Apache: Options -Indexes . nginx: autoindex off; (the default). Add an empty index.html to folders that must stay browsable-free.",
107 verify: "curl -s https://<site>/uploads/ | grep -i 'index of' should print nothing.",
108 refs: []check.Ref{{Title: "Apache HTTP Server, Options directive", URL: "https://httpd.apache.org/docs/2.4/mod/core.html#options"}},
109 std: check.Standards{CWE: []string{"CWE-548"}, OWASP: []string{"A01:2021", "A05:2021"}, ASVS: []string{"4.3.2"}, WSTG: []string{"WSTG-CONF-04"}, NIST: []string{"CM-7", "AC-3"}},
110 probes: []probe{{"/", bodyHas("<title>Index of /")}, {"/uploads/", bodyHas("<title>Index of /")}, {"/images/", bodyHas("<title>Index of /")},
111 {"/files/", bodyHas("<title>Index of /")}, {"/backup/", bodyHas("<title>Index of /")}, {"/wp-content/uploads/", bodyHas("<title>Index of /")}},
112 },
113 {
114 id: "WEB-EXP-006", title: "Web Server Status Page Exposed", sev: check.Medium,
115 cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
116 summary: "Checks whether Apache server-status/server-info or nginx stub_status are reachable for each site.",
117 rationale: "Status pages show live requests (including URLs with tokens), client IPs, virtual hosts and modules.",
118 detection: "Requests /server-status, /server-info and /nginx_status for each site through 127.0.0.1. Because the scan itself comes from 127.0.0.1, a page that answers is checked against the web server configuration: when the status location carries Require local, Require ip 127.0.0.1 or allow 127.0.0.1 with deny all, the page is reachable from this machine only and the check passes with that noted (confirm from another machine); otherwise it fails.",
119 fix: "Restrict to administrators: <Location /server-status> Require local </Location> (Apache), or allow 127.0.0.1; deny all; (nginx). Remove the module if unused.",
120 verify: "From another machine: curl -s -o /dev/null -w '%{http_code}' https://<site>/server-status should print 403 or 404.",
121 refs: []check.Ref{{Title: "Apache HTTP Server, mod_status", URL: "https://httpd.apache.org/docs/2.4/mod/mod_status.html"}},
122 std: check.Standards{CWE: []string{"CWE-497"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.3.2"}, NIST: []string{"CM-7", "SI-11"}},
123 probes: []probe{{"/server-status", bodyHas("Apache Server Status")}, {"/server-info", bodyHas("Apache Server Information")},
124 {"/nginx_status", bodyHas("Active connections:")}},
125 },
126}
127
128func init() {
129 for _, r := range exposureRules {
130 registerExposure(r)
131 }
132}
133
134// registerExposure turns one exposure rule into a registered check.
135func registerExposure(r exposureRule) {
136 {
137 check.Register(&check.Check{
138 ID: r.id, Title: r.title, Domain: check.DomainWeb, Severity: r.sev, Category: r.cat, Effort: r.effort, CVSS: r.cvss,
139 Summary: r.summary, Rationale: r.rationale, Detection: r.detection, Fix: r.fix, Verify: r.verify,
140 Refs: r.refs, Standards: r.std,
141 Run: func(ctx context.Context, s sys.System) []check.Result {
142 return ForEachSite(ctx, s, func(st Site) check.Result {
143 var hits []string
144 for _, p := range r.probes {
145 resp, err := st.Get(ctx, s, p.path)
146 if err == nil && p.marker(resp) {
147 hits = append(hits, p.path+" (HTTP 200)")
148 }
149 }
150 if len(hits) > 0 {
151 if r.id == "WEB-EXP-006" {
152 if ev, ok := collect.StatusPageLocalOnly(ctx, s); ok {
153 return check.PassR(st.Name, "Status page answers from 127.0.0.1 but the configuration restricts it to local clients; confirm from another machine", append(hits, "restriction: "+ev)...)
154 }
155 hits = append(hits, "no local-only restriction (Require local, Require ip 127.0.0.1 or allow 127.0.0.1 with deny all) found in the web server configuration; the scan requests from 127.0.0.1, so confirm from another machine")
156 }
157 return check.FailR(st.Name, "Exposed: "+strings.Join(hits, ", "), hits...)
158 }
159 return check.PassR(st.Name, "Not exposed")
160 })
161 },
162 })
163 }
164}

Also available as plain Markdown for notes, scripts and AI tools. Same facts, same source, no styling.

Cite This Page

If this page helped your work, please cite it. The version and web address are filled in; the access date is today's date in your browser. Every other style, and when credit is required, is on How to Cite OpenVAPT.

APA 7
OpenVAPT. (2026). WEB-EXP-009 Dependency Manifests Exposed (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/WEB-EXP-009.html

In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.

BibTeX
@misc{openvapt_web_exp_009,
  author       = {{OpenVAPT}},
  title        = {{WEB-EXP-009 Dependency Manifests Exposed}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/WEB-EXP-009.html}},
  note         = {OpenVAPT Check Catalogue, version 0.1.0-alpha.6. Accessed 16 October 2026}
}

Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.

Citing OpenVAPT does not mean OpenVAPT endorses, approves or has reviewed your work.