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
HighWEB-DOC-001Information ExposureWeb Apps

Secrets or Backups Stored Inside a Web Root

Looks on disk inside each web document root for environment files, database dumps, backups, archives and private keys.

Why It Matters
Anything inside a document root may be downloadable, now or after a configuration change. Dumps, backups and keys there are a leading cause of data breaches; finding them on disk catches files the URL checks cannot guess.
How It Detects
Reads document roots from the local Apache, nginx and LiteSpeed configuration, walks each one (metadata only, up to 50000 files, symlinks not followed) and matches file names such as .env, *.sql, *.bak, backup*.tar.gz, id_rsa, *key*.pem and *.key. File contents are never read. Plain archives and certificates (*.tar.gz, *.pem) are reviewed separately at Medium by WEB-DOC-003, because they are often published on purpose.
How to Fix
Move each listed file outside the document root (for example to a 700 folder in the account's home), delete what is not needed, and rotate any secret or key that was exposed. Keep backups in a dedicated location outside every web root.
How to Verify
Re-run this check; no files should be listed.
CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/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-530, CWE-538, CWE-312
OWASP Top 10
A05:2021
OWASP ASVS 4.0.3
12.5.1
NIST SP 800-53 Rev. 5
SC-28, CM-6
References
OWASP WSTG-CONF-04, Review Old Backup and Unreferenced Files for Sensitive Information
OWASP Secrets Management Cheat Sheet
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-DOC-001, or read it offline with openvapt explain WEB-DOC-001.

internal/checks/web/docroot.go · Lines 1-259View on GitHub at This Commit
1package web
2
3import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "strings"
8
9 "github.com/openvapt/openvapt/internal/check"
10 "github.com/openvapt/openvapt/internal/checks/collect"
11 "github.com/openvapt/openvapt/internal/sys"
12)
13
14// Sensitive file name patterns that should never sit inside a web root.
15var sensitiveNames = []struct{ pattern, why string }{
16 {".env", "environment secrets"}, {".env.*", "environment secrets"}, {"*.sql", "database dump"}, {"*.sql.gz", "database dump"},
17 {"*.bak", "backup copy"}, {"*.old", "backup copy"}, {"*.orig", "backup copy"}, {"*~", "editor backup"},
18 {"*.swp", "editor swap file"}, {"backup*.zip", "backup archive"}, {"backup*.tar.gz", "backup archive"}, {"backup*.tgz", "backup archive"},
19 {"*dump*.tar.gz", "database dump archive"}, {"*dump*.tgz", "database dump archive"}, {"*dump*.zip", "database dump archive"},
20 {"id_rsa", "SSH private key"}, {"id_ed25519", "SSH private key"}, {"id_ecdsa", "SSH private key"},
21 {"*key*.pem", "private key"}, {"*priv*.pem", "private key"}, {"*.key", "private key"}, {"*.p12", "key store"}, {"*.pfx", "key store"}, {"wp-config.php.*", "WordPress config copy"},
22}
23
24// reviewNames are files that are often legitimate inside a web root (a
25// published certificate chain, a downloadable release archive) but deserve a
26// look, because the same names also carry private keys and site backups.
27// They are rated Medium by WEB-DOC-003, not High by WEB-DOC-001 (C-09).
28var reviewNames = []struct{ pattern, why string }{
29 {"*.pem", "certificate or key material; confirm it holds no private key"}, {"*.crt", "certificate; confirm the key is not beside it"},
30 {"*.tar.gz", "archive; confirm it is a published download, not a site backup"}, {"*.tgz", "archive; confirm it is a published download, not a site backup"},
31 {"*.tar", "archive; confirm it is a published download, not a site backup"}, {"*.7z", "archive; confirm it is a published download, not a site backup"},
32}
33
34// downloadFolder reports a path under a folder whose purpose is publishing
35// files, where archives are expected.
36func downloadFolder(path string) bool {
37 l := strings.ToLower(path)
38 for _, d := range []string{"/download/", "/downloads/", "/releases/", "/dist/", "/.well-known/"} {
39 if strings.Contains(l, d) {
40 return true
41 }
42 }
43 return false
44}
45
46func review(name string) (string, bool) {
47 if _, hi := sensitive(name); hi {
48 return "", false
49 }
50 for _, s := range reviewNames {
51 if ok, _ := filepath.Match(s.pattern, name); ok {
52 return s.why, true
53 }
54 }
55 return "", false
56}
57
58func sensitive(name string) (string, bool) {
59 for _, s := range sensitiveNames {
60 if ok, _ := filepath.Match(s.pattern, name); ok {
61 return s.why, true
62 }
63 }
64 return "", false
65}
66
67func init() {
68 check.Register(&check.Check{
69 ID: "WEB-DOC-001", Title: "Secrets or Backups Stored Inside a Web Root", Domain: check.DomainWeb,
70 Severity: check.High, Category: check.CatExposure, Effort: check.EffortLow,
71 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
72 Summary: "Looks on disk inside each web document root for environment files, database dumps, backups, archives and private keys.",
73 Rationale: "Anything inside a document root may be downloadable, now or after a configuration change. Dumps, backups and keys there are a leading cause of data breaches; finding them on disk catches files the URL checks cannot guess.",
74 Detection: "Reads document roots from the local Apache, nginx and LiteSpeed configuration, walks each one (metadata only, up to 50000 files, symlinks not followed) and matches file names such as .env, *.sql, *.bak, backup*.tar.gz, id_rsa, *key*.pem and *.key. File contents are never read. Plain archives and certificates (*.tar.gz, *.pem) are reviewed separately at Medium by WEB-DOC-003, because they are often published on purpose.",
75 Fix: "Move each listed file outside the document root (for example to a 700 folder in the account's home), delete what is not needed, and rotate any secret or key that was exposed. Keep backups in a dedicated location outside every web root.",
76 Verify: "Re-run this check; no files should be listed.",
77 Refs: []check.Ref{refWSTGConf04, {Title: "OWASP Secrets Management Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"}},
78 Standards: check.Standards{CWE: []string{"CWE-530", "CWE-538", "CWE-312"}, OWASP: []string{"A05:2021"}, ASVS: []string{"12.5.1"}, WSTG: []string{"WSTG-CONF-04"}, NIST: []string{"SC-28", "CM-6"}},
79 Run: func(ctx context.Context, s sys.System) []check.Result {
80 roots := collect.DocRoots(ctx, s)
81 if len(roots) == 0 {
82 return []check.Result{check.SkipR("web roots", "No document roots found in the local web server configuration")}
83 }
84 var out []check.Result
85 for _, root := range roots {
86 w, err := s.Walk(ctx, root, 50000)
87 if err != nil && len(w.Files) == 0 {
88 continue
89 }
90 var hits []string
91 for _, f := range w.Files {
92 if why, ok := sensitive(filepath.Base(f.Path)); ok {
93 hits = append(hits, f.Path+" ("+why+")")
94 }
95 }
96 switch {
97 case len(hits) > 0:
98 if len(hits) > 30 {
99 hits = append(hits[:30], fmt.Sprintf("... and %d more", len(hits)-30))
100 }
101 out = append(out, check.FailR(root, fmt.Sprintf("%d sensitive file(s) inside the web root", len(hits)), hits...))
102 case !w.Complete:
103 out = append(out, check.IncompleteR(root, "Web root could not be fully walked"))
104 default:
105 out = append(out, check.PassR(root, fmt.Sprintf("No sensitive files among %d checked", len(w.Files))))
106 }
107 }
108 if len(out) == 0 {
109 return []check.Result{check.SkipR("web roots", "Document roots could not be read")}
110 }
111 return out
112 },
113 })
114
115 check.Register(&check.Check{
116 ID: "WEB-DOC-003", Title: "Archives or Certificates Inside a Web Root", Domain: check.DomainWeb,
117 Severity: check.Medium, Category: check.CatExposure, Effort: check.EffortLow,
118 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
119 Summary: "Lists archives (*.tar.gz, *.tgz, *.tar, *.7z) and certificate files (*.pem, *.crt) inside each web document root for review, outside download folders.",
120 Rationale: "A tarball inside a web root is often a site or database backup left after a migration, and a .pem file sometimes carries the private key next to the certificate. Both are downloadable by anyone who guesses the name. But download sites publish archives on purpose and public certificates are harmless, so this is a Medium review item, not a High finding; names that clearly mean backup, dump or key are rated High by WEB-DOC-001.",
121 Detection: "Walks each document root (metadata only, up to 50000 files, symlinks not followed) and lists files matching the archive and certificate patterns that WEB-DOC-001 does not already rate High. Files under /download/, /downloads/, /releases/, /dist/ or /.well-known/ are noted as expected publications and do not fail. File contents are never read.",
122 Fix: "Confirm each file is meant to be public. Move backups and anything holding a private key outside the document root, rotate any exposed key, and keep published downloads in a dedicated folder such as /downloads/.",
123 Verify: "Re-run this check; only files you have confirmed as intended downloads should remain, in a download folder.",
124 Refs: []check.Ref{refWSTGConf04, {Title: "OWASP Secrets Management Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"}},
125 Standards: check.Standards{CWE: []string{"CWE-538", "CWE-552"}, OWASP: []string{"A05:2021"}, ASVS: []string{"12.5.1"}, WSTG: []string{"WSTG-CONF-04"}, NIST: []string{"SC-28", "CM-6"}},
126 Run: func(ctx context.Context, s sys.System) []check.Result {
127 roots := collect.DocRoots(ctx, s)
128 if len(roots) == 0 {
129 return []check.Result{check.SkipR("web roots", "No document roots found in the local web server configuration")}
130 }
131 var out []check.Result
132 for _, root := range roots {
133 w, err := s.Walk(ctx, root, 50000)
134 if err != nil && len(w.Files) == 0 {
135 continue
136 }
137 var hits, expected []string
138 for _, f := range w.Files {
139 why, ok := review(filepath.Base(f.Path))
140 if !ok {
141 continue
142 }
143 if downloadFolder(f.Path) {
144 expected = append(expected, f.Path+" (in a download folder, expected)")
145 continue
146 }
147 hits = append(hits, f.Path+" ("+why+")")
148 }
149 switch {
150 case len(hits) > 0:
151 if len(hits) > 30 {
152 hits = append(hits[:30], fmt.Sprintf("... and %d more", len(hits)-30))
153 }
154 out = append(out, check.FailR(root, fmt.Sprintf("%d archive or certificate file(s) inside the web root to review", len(hits)), append(hits, expected...)...))
155 case !w.Complete:
156 out = append(out, check.IncompleteR(root, "Web root could not be fully walked"))
157 default:
158 out = append(out, check.PassR(root, fmt.Sprintf("No archives or certificates to review among %d files", len(w.Files)), expected...))
159 }
160 }
161 if len(out) == 0 {
162 return []check.Result{check.SkipR("web roots", "Document roots could not be read")}
163 }
164 return out
165 },
166 })
167
168 check.Register(&check.Check{
169 ID: "WEB-DOC-002", Title: "Web Root Files Writable by Other Users", Domain: check.DomainWeb,
170 Severity: check.High, Category: check.CatAccess, Effort: check.EffortLow,
171 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
172 Summary: "Looks for world-writable files inside web document roots.",
173 Rationale: "A world-writable PHP or HTML file lets any local user, or any other compromised site on the same server, plant a web shell or inject malware into pages served to visitors.",
174 Detection: "Walks each document root (metadata only, up to 50000 files, symlinks not followed) and lists regular files with the 'other write' permission bit.",
175 Fix: "chmod o-w <file> for each listed file (find <docroot> -type f -perm -0002 -exec chmod o-w {} +). Use 644 for files and 755 for folders, owned by the site account, and grant the web server write access only to upload folders.",
176 Verify: "find <docroot> -type f -perm -0002 should print nothing.",
177 Refs: []check.Ref{{Title: "OWASP WSTG-CONF-09, Test File Permission", URL: "https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/09-Test_File_Permission/"}},
178 Standards: check.Standards{CWE: []string{"CWE-732"}, OWASP: []string{"A05:2021"}, WSTG: []string{"WSTG-CONF-09"}, NIST: []string{"AC-3", "AC-6"}},
179 Run: func(ctx context.Context, s sys.System) []check.Result {
180 roots := collect.DocRoots(ctx, s)
181 if len(roots) == 0 {
182 return []check.Result{check.SkipR("web roots", "No document roots found in the local web server configuration")}
183 }
184 var out []check.Result
185 for _, root := range roots {
186 w, err := s.Walk(ctx, root, 50000)
187 if err != nil && len(w.Files) == 0 {
188 continue
189 }
190 var bad []string
191 for _, f := range w.Files {
192 if f.Mode.Perm()&0o002 != 0 {
193 bad = append(bad, fmt.Sprintf("%04o %s", f.Mode.Perm(), f.Path))
194 }
195 }
196 switch {
197 case len(bad) > 0:
198 if len(bad) > 30 {
199 bad = append(bad[:30], fmt.Sprintf("... and %d more", len(bad)-30))
200 }
201 out = append(out, check.FailR(root, fmt.Sprintf("%d world-writable file(s) in the web root", len(bad)), bad...))
202 case !w.Complete:
203 out = append(out, check.IncompleteR(root, "Web root could not be fully walked"))
204 default:
205 out = append(out, check.PassR(root, "No world-writable files"))
206 }
207 }
208 if len(out) == 0 {
209 return []check.Result{check.SkipR("web roots", "Document roots could not be read")}
210 }
211 return out
212 },
213 })
214
215 check.Register(&check.Check{
216 ID: "WEB-COR-001", Title: "Cross-Origin Resource Sharing Allows Any Origin With Credentials", Domain: check.DomainWeb,
217 Severity: check.Medium, Category: check.CatWebApp, Effort: check.EffortLow,
218 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N",
219 Summary: "Checks the home page's CORS headers for a wildcard origin combined with credentials, or an origin of null.",
220 Rationale: "A permissive CORS policy can let any website read responses on behalf of a logged-in visitor, exposing personal data and tokens.",
221 Detection: "Reads Access-Control-Allow-Origin and Access-Control-Allow-Credentials on each site's home page. '*' with credentials true, or 'null', fails. Origin reflection needs crafted requests and is outside this read-only check.",
222 Fix: "Return Access-Control-Allow-Origin only for an explicit allow-list of trusted origins, never '*' or 'null' together with credentials, and add Vary: Origin.",
223 Verify: "curl -sI https://<site>/ | grep -i access-control",
224 Refs: []check.Ref{{Title: "OWASP WSTG-CLNT-07, Testing Cross Origin Resource Sharing", URL: "https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/11-Client-side_Testing/07-Testing_Cross_Origin_Resource_Sharing/"}, refMDN("Access-Control-Allow-Origin")},
225 Standards: check.Standards{CWE: []string{"CWE-942"}, OWASP: []string{"A05:2021", "A01:2021"}, ASVS: []string{"14.5.3"}, WSTG: []string{"WSTG-CLNT-07"}, NIST: []string{"AC-4", "CM-6"}},
226 Run: func(ctx context.Context, s sys.System) []check.Result {
227 return ForEachSite(ctx, s, func(st Site) check.Result {
228 o := strings.TrimSpace(st.Home.Header.Get("Access-Control-Allow-Origin"))
229 cred := strings.EqualFold(strings.TrimSpace(st.Home.Header.Get("Access-Control-Allow-Credentials")), "true")
230 if o == "null" || (o == "*" && cred) {
231 return check.FailR(st.Name, "Permissive CORS policy", "Access-Control-Allow-Origin: "+o, fmt.Sprintf("Access-Control-Allow-Credentials: %v", cred))
232 }
233 return check.PassR(st.Name, "No permissive CORS policy on the home page")
234 })
235 },
236 })
237
238 check.Register(&check.Check{
239 ID: "WEB-SEC-001", Title: "No security.txt Contact File", Domain: check.DomainWeb,
240 Severity: check.Info, Category: check.CatWebApp, Effort: check.EffortLow,
241 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N",
242 Summary: "Checks whether each site publishes /.well-known/security.txt (RFC 9116).",
243 Rationale: "security.txt tells researchers how to report a vulnerability. Without it, reports may go unheard or be disclosed publicly instead.",
244 Detection: "Requests /.well-known/security.txt and looks for a Contact: field. This is informational and does not affect the risk summary.",
245 Fix: "Publish /.well-known/security.txt with at least Contact: and Expires: fields (see securitytxt.org).",
246 Verify: "curl -s https://<site>/.well-known/security.txt",
247 Refs: []check.Ref{{Title: "RFC 9116, A File Format to Aid in Security Vulnerability Disclosure", URL: "https://www.rfc-editor.org/rfc/rfc9116.html"}},
248 Standards: check.Standards{CWE: []string{"CWE-1188"}, NIST: []string{"RA-5(11)"}},
249 Run: func(ctx context.Context, s sys.System) []check.Result {
250 return ForEachSite(ctx, s, func(st Site) check.Result {
251 r, err := st.Get(ctx, s, "/.well-known/security.txt")
252 if err == nil && r.Status == 200 && strings.Contains(strings.ToLower(r.Body), "contact:") {
253 return check.PassR(st.Name, "security.txt is published")
254 }
255 return check.FailR(st.Name, "No /.well-known/security.txt with a Contact field")
256 })
257 },
258 })
259}

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-DOC-001 Secrets or Backups Stored Inside a Web Root (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/WEB-DOC-001.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_doc_001,
  author       = {{OpenVAPT}},
  title        = {{WEB-DOC-001 Secrets or Backups Stored Inside a Web Root}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/WEB-DOC-001.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.