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
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
| 1 | package web |
| 2 | |
| 3 | import ( |
| 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"). |
| 15 | type probe struct { |
| 16 | path string |
| 17 | marker func(sys.HTTPResponse) bool |
| 18 | } |
| 19 | |
| 20 | func 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. |
| 35 | func binaryHit(r sys.HTTPResponse) bool { return r.Status == 200 && !HTML(r) && len(r.Body) > 0 } |
| 36 | |
| 37 | type 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 | |
| 48 | var 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 | |
| 128 | func init() { |
| 129 | for _, r := range exposureRules { |
| 130 | registerExposure(r) |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | // registerExposure turns one exposure rule into a registered check. |
| 135 | func 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.
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.
@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.