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

Laravel Debug Mode On or Application Key Missing

Checks Laravel .env files for APP_DEBUG=true or an empty APP_KEY.

Why It Matters
With APP_DEBUG on, Laravel's error page prints the environment, including database and mail passwords, to any visitor who triggers an error. APP_KEY encrypts sessions and cookies; with it empty or default the application refuses to run or signs nothing.
How It Detects
Finds .env files in /srv, /opt, /var/www, home folders and web roots (three folders deep) that contain APP_KEY (the Laravel marker) and fails when APP_DEBUG is true or APP_KEY is empty. Values are never reported.
How to Fix
Set APP_DEBUG=false and APP_ENV=production in .env, run php artisan key:generate if APP_KEY is empty, then php artisan config:cache. Keep .env outside the web root (Laravel's public/ folder) and readable only by the application user.
How to Verify
grep -E '^APP_(DEBUG|KEY)=' .env shows APP_DEBUG=false and a base64 key.
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
16.7
NIST CSF 2.0 Subcategory
PR.PS-01
OWASP WSTG v4.2
WSTG-CONF-02, WSTG-ERRH-01
CWE
CWE-489, CWE-209, CWE-321
OWASP Top 10
A05:2021
OWASP ASVS 4.0.3
14.3.2, 2.10.4
NIST SP 800-53 Rev. 5
CM-6, SI-11, IA-5
References
Laravel documentation, Deployment
Laravel documentation, Configuration (debug mode, environment files)
OWASP ASVS 4.0.3, V14 Configuration
Learn the theory behind this check: A6 Application Framework and CMS Configuration.

Source Code

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

internal/checks/web/frameworks.go · Lines 1-461View on GitHub at This Commit
1package web
2
3import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "regexp"
8 "sort"
9 "strings"
10 "time"
11
12 "github.com/openvapt/openvapt/internal/check"
13 "github.com/openvapt/openvapt/internal/checks/collect"
14 "github.com/openvapt/openvapt/internal/sys"
15)
16
17// Application framework configuration checks (WEB-APP). Laravel, Symfony,
18// Rails, Spring Boot and Flask projects are found through the same project
19// folders as the Django checks (collect.ProjectFiles). Configuration files
20// hold secrets, so only the path and the name of the setting are reported,
21// never a value; files under writable project folders are read without
22// following symlinks.
23
24var (
25 refLaravelDeploy = check.Ref{Title: "Laravel documentation, Deployment", URL: "https://laravel.com/docs/12.x/deployment"}
26 refLaravelConfig = check.Ref{Title: "Laravel documentation, Configuration (debug mode, environment files)", URL: "https://laravel.com/docs/12.x/configuration"}
27 refSymfonyEnv = check.Ref{Title: "Symfony documentation, Configuring Symfony (environments)", URL: "https://symfony.com/doc/current/configuration.html"}
28 refRailsSecurity = check.Ref{Title: "Ruby on Rails Guides, Securing Rails Applications", URL: "https://guides.rubyonrails.org/security.html"}
29 refRailsConfig = check.Ref{Title: "Ruby on Rails Guides, Configuring Rails Applications", URL: "https://guides.rubyonrails.org/configuring.html"}
30 refSpringAct = check.Ref{Title: "Spring Boot documentation, Actuator endpoints", URL: "https://docs.spring.io/spring-boot/reference/actuator/endpoints.html"}
31 refFlaskDebug = check.Ref{Title: "Flask documentation, Debug mode", URL: "https://flask.palletsprojects.com/en/stable/quickstart/#debug-mode"}
32 refASVSV14 = check.Ref{Title: "OWASP ASVS 4.0.3, V14 Configuration", URL: "https://github.com/OWASP/ASVS/blob/v4.0.3/4.0/en/0x22-V14-Config.md"}
33 refWSTGConf02 = check.Ref{Title: "OWASP WSTG-CONF-02, Test Application Platform Configuration", URL: "https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/02-Test_Application_Platform_Configuration/"}
34 refSecretsCS = check.Ref{Title: "OWASP Secrets Management Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"}
35 refOWASPDepCheck = check.Ref{Title: "OWASP Vulnerable Dependency Management Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Vulnerable_Dependency_Management_Cheat_Sheet.html"}
36)
37
38// envFile is one dotenv file found in a project folder.
39type envFile struct {
40 path string
41 vars map[string]string // upper-cased key -> value, comments removed
42 mode sys.FileInfo
43}
44
45// parseDotenv reads KEY=value lines. Quotes are stripped; values are kept
46// only in memory for comparisons and never reported.
47func parseDotenv(text string) map[string]string {
48 out := map[string]string{}
49 for _, line := range strings.Split(text, "\n") {
50 t := strings.TrimSpace(line)
51 if t == "" || strings.HasPrefix(t, "#") {
52 continue
53 }
54 t = strings.TrimPrefix(t, "export ")
55 k, v, ok := strings.Cut(t, "=")
56 if !ok {
57 continue
58 }
59 v = strings.TrimSpace(v)
60 if q := v[:min(len(v), 1)]; q == `"` || q == `'` {
61 // Quoted value: keep everything up to the closing quote.
62 if end := strings.Index(v[1:], q); end >= 0 {
63 v = v[1 : end+1]
64 } else {
65 v = strings.Trim(v, q)
66 }
67 } else if i := strings.Index(v, " #"); i >= 0 {
68 v = strings.TrimSpace(v[:i])
69 }
70 out[strings.ToUpper(strings.TrimSpace(k))] = v
71 }
72 return out
73}
74
75// envFiles returns every .env file in the project folders that contains at
76// least one of the marker keys (so a Laravel check never reads a Symfony
77// file as Laravel and vice versa).
78func envFiles(ctx context.Context, s sys.System, markers ...string) []envFile {
79 var out []envFile
80 for _, p := range collect.ProjectFiles(ctx, s, ".env") {
81 r, err := sys.ReadFileNoFollow(ctx, s, p)
82 if err != nil {
83 continue
84 }
85 vars := parseDotenv(string(r.Data))
86 hit := len(markers) == 0
87 for _, m := range markers {
88 if _, ok := vars[m]; ok {
89 hit = true
90 }
91 }
92 if !hit {
93 continue
94 }
95 st, _ := s.Stat(p)
96 out = append(out, envFile{path: p, vars: vars, mode: st})
97 }
98 return out
99}
100
101func truthy(v string) bool {
102 switch strings.ToLower(strings.TrimSpace(v)) {
103 case "true", "1", "yes", "on":
104 return true
105 }
106 return false
107}
108
109// unreadable is the verdict a judge returns when its file could not be read;
110// perFile reports it as Incomplete rather than Fail.
111const unreadable = "could not be read"
112
113// perFile turns per-path verdicts into results, or a Skip when nothing was found.
114func perFile(paths []string, judge func(string) []string, failMsg, skipMsg string) []check.Result {
115 if len(paths) == 0 {
116 return []check.Result{check.SkipR("projects", skipMsg)}
117 }
118 var out []check.Result
119 for _, p := range paths {
120 if bad := judge(p); len(bad) == 1 && bad[0] == unreadable {
121 out = append(out, check.IncompleteR(p, "File could not be read"))
122 } else if len(bad) > 0 {
123 out = append(out, check.FailR(p, failMsg, bad...))
124 } else {
125 out = append(out, check.PassR(p, "No issue found"))
126 }
127 }
128 return out
129}
130
131func init() {
132 check.Register(&check.Check{
133 ID: "WEB-APP-001", Title: "Laravel Debug Mode On or Application Key Missing", Domain: check.DomainWeb,
134 Severity: check.High, Category: check.CatExposure, Effort: check.EffortLow,
135 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
136 Summary: "Checks Laravel .env files for APP_DEBUG=true or an empty APP_KEY.",
137 Rationale: "With APP_DEBUG on, Laravel's error page prints the environment, including database and mail passwords, to any visitor who triggers an error. APP_KEY encrypts sessions and cookies; with it empty or default the application refuses to run or signs nothing.",
138 Detection: "Finds .env files in /srv, /opt, /var/www, home folders and web roots (three folders deep) that contain APP_KEY (the Laravel marker) and fails when APP_DEBUG is true or APP_KEY is empty. Values are never reported.",
139 Fix: "Set APP_DEBUG=false and APP_ENV=production in .env, run php artisan key:generate if APP_KEY is empty, then php artisan config:cache. Keep .env outside the web root (Laravel's public/ folder) and readable only by the application user.",
140 Verify: "grep -E '^APP_(DEBUG|KEY)=' .env shows APP_DEBUG=false and a base64 key.",
141 Refs: []check.Ref{refLaravelDeploy, refLaravelConfig, refASVSV14},
142 Standards: check.Standards{CWE: []string{"CWE-489", "CWE-209", "CWE-321"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.3.2", "2.10.4"}, WSTG: []string{"WSTG-CONF-02", "WSTG-ERRH-01"}, NIST: []string{"CM-6", "SI-11", "IA-5"}},
143 Run: func(ctx context.Context, s sys.System) []check.Result {
144 files := envFiles(ctx, s, "APP_KEY")
145 var paths []string
146 byPath := map[string]envFile{}
147 for _, f := range files {
148 paths = append(paths, f.path)
149 byPath[f.path] = f
150 }
151 return perFile(paths, func(p string) []string {
152 f := byPath[p]
153 var bad []string
154 if truthy(f.vars["APP_DEBUG"]) {
155 bad = append(bad, "APP_DEBUG is true")
156 }
157 if strings.TrimSpace(f.vars["APP_KEY"]) == "" {
158 bad = append(bad, "APP_KEY is empty")
159 }
160 return bad
161 }, "Laravel runs in debug mode or without an application key", "No Laravel .env files found")
162 },
163 })
164
165 check.Register(&check.Check{
166 ID: "WEB-APP-002", Title: "Application .env File Readable by All Users", Domain: check.DomainWeb,
167 Severity: check.Medium, Category: check.CatAccess, Effort: check.EffortLow,
168 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
169 Summary: "Checks the permission bits of framework .env files for world-read access.",
170 Rationale: ".env files hold database passwords, API tokens and signing keys in plain text. A world-readable .env hands every account on the server, including other websites' PHP-FPM pools, the keys to this application.",
171 Detection: "Stats each .env file found in the project folders (metadata only) and fails when the 'other read' permission bit is set.",
172 Fix: "chmod 640 .env && chown <app-user>:<web-group> .env (or 600 when the application runs as its own user). Rotate the secrets if other people have had shell access.",
173 Verify: "stat -c '%a %U:%G %n' .env shows 640 or 600.",
174 Refs: []check.Ref{refSecretsCS, refASVSV14},
175 Standards: check.Standards{CWE: []string{"CWE-732", "CWE-312"}, OWASP: []string{"A05:2021", "A02:2021"}, ASVS: []string{"14.1.3"}, WSTG: []string{"WSTG-CONF-09"}, NIST: []string{"AC-3", "SC-28"}},
176 Run: func(ctx context.Context, s sys.System) []check.Result {
177 files := envFiles(ctx, s)
178 var paths []string
179 byPath := map[string]envFile{}
180 for _, f := range files {
181 paths = append(paths, f.path)
182 byPath[f.path] = f
183 }
184 return perFile(paths, func(p string) []string {
185 if byPath[p].mode.Mode.Perm()&0o004 != 0 {
186 return []string{fmt.Sprintf("mode %04o is readable by all users", byPath[p].mode.Mode.Perm())}
187 }
188 return nil
189 }, ".env file is readable by all users", "No .env files found in the project folders")
190 },
191 })
192
193 check.Register(&check.Check{
194 ID: "WEB-APP-003", Title: "Symfony Running in the Development Environment", Domain: check.DomainWeb,
195 Severity: check.High, Category: check.CatExposure, Effort: check.EffortLow,
196 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
197 Summary: "Checks Symfony .env files for APP_ENV=dev or APP_DEBUG=1.",
198 Rationale: "The dev environment enables the web profiler and debug toolbar, which show requests, queries, configuration, the container and sometimes credentials to any visitor, and it skips the caches that production relies on.",
199 Detection: "Finds .env files that contain APP_SECRET (the Symfony marker) and fails when APP_ENV is dev or test, or APP_DEBUG is 1 or true. A .env.local or .env.prod override is not evaluated, so confirm the final value on the server.",
200 Fix: "Set APP_ENV=prod and APP_DEBUG=0 in .env (or .env.local), run composer dump-env prod, clear the cache (php bin/console cache:clear) and remove the web profiler bundle from production.",
201 Verify: "php bin/console about shows Environment prod and Debug false.",
202 Refs: []check.Ref{refSymfonyEnv, {Title: "Symfony documentation, How to Deploy a Symfony Application", URL: "https://symfony.com/doc/current/deployment.html"}, refASVSV14},
203 Standards: check.Standards{CWE: []string{"CWE-489", "CWE-215"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.3.2"}, WSTG: []string{"WSTG-CONF-02", "WSTG-ERRH-01"}, NIST: []string{"CM-6", "SI-11"}},
204 Run: func(ctx context.Context, s sys.System) []check.Result {
205 files := envFiles(ctx, s, "APP_SECRET")
206 var paths []string
207 byPath := map[string]envFile{}
208 for _, f := range files {
209 paths = append(paths, f.path)
210 byPath[f.path] = f
211 }
212 return perFile(paths, func(p string) []string {
213 f := byPath[p]
214 var bad []string
215 if e := strings.ToLower(f.vars["APP_ENV"]); e == "dev" || e == "test" {
216 bad = append(bad, "APP_ENV is "+e)
217 }
218 if truthy(f.vars["APP_DEBUG"]) {
219 bad = append(bad, "APP_DEBUG is on")
220 }
221 return bad
222 }, "Symfony runs in the development environment", "No Symfony .env files found")
223 },
224 })
225
226 check.Register(&check.Check{
227 ID: "WEB-APP-004", Title: "Rails Master Key or Secrets File Exposed", Domain: check.DomainWeb,
228 Severity: check.High, Category: check.CatAccess, Effort: check.EffortLow,
229 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
230 Summary: "Checks Rails config/master.key, credentials key files and secrets.yml for world-read permissions or a location inside a web root.",
231 Rationale: "master.key decrypts credentials.yml.enc, which holds every production secret, and secrets.yml holds the secret_key_base that signs sessions. Anyone who reads them can forge sessions and read the database password. Both are meant to stay out of version control and out of any public folder.",
232 Detection: "Finds config/master.key, config/credentials/*.key and config/secrets.yml in the project folders and fails when the file is readable by others or sits inside a web document root. Contents are never read.",
233 Fix: "chmod 600 config/master.key config/credentials/*.key; keep them out of git (.gitignore) and out of public/; load the key from RAILS_MASTER_KEY in the environment on servers. Rotate the credentials if the key was ever exposed.",
234 Verify: "stat -c '%a %n' config/master.key shows 600, and git ls-files config/master.key prints nothing.",
235 Refs: []check.Ref{refRailsSecurity, {Title: "Ruby on Rails Guides, Custom Credentials", URL: "https://guides.rubyonrails.org/security.html#custom-credentials"}, refSecretsCS},
236 Standards: check.Standards{CWE: []string{"CWE-732", "CWE-312", "CWE-321"}, OWASP: []string{"A02:2021", "A05:2021"}, ASVS: []string{"2.10.4", "14.1.3"}, WSTG: []string{"WSTG-CONF-09"}, NIST: []string{"AC-3", "SC-28", "IA-5"}},
237 Run: func(ctx context.Context, s sys.System) []check.Result {
238 paths := collect.ProjectFiles(ctx, s, "config/master.key", "config/credentials/*.key", "config/secrets.yml")
239 roots := collect.DocRoots(ctx, s)
240 return perFile(paths, func(p string) []string {
241 var bad []string
242 if st, err := s.Stat(p); err == nil && st.Mode.Perm()&0o004 != 0 {
243 bad = append(bad, fmt.Sprintf("mode %04o is readable by all users", st.Mode.Perm()))
244 }
245 for _, r := range roots {
246 if strings.HasPrefix(p, r+"/") {
247 bad = append(bad, "inside the web root "+r)
248 }
249 }
250 return bad
251 }, "Rails key or secrets file is exposed", "No Rails master.key or secrets.yml found")
252 },
253 })
254
255 check.Register(&check.Check{
256 ID: "WEB-APP-005", Title: "Rails Production Does Not Force HTTPS", Domain: check.DomainWeb,
257 Severity: check.Medium, Category: check.CatWebApp, Effort: check.EffortLow,
258 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N",
259 Summary: "Checks config/environments/production.rb for config.force_ssl = true.",
260 Rationale: "force_ssl redirects plain HTTP to HTTPS, marks cookies Secure and sends Strict-Transport-Security. Without it a session cookie can travel over HTTP once and be read on the network, even when the site also works over HTTPS.",
261 Detection: "Finds config/environments/production.rb in the project folders and fails when config.force_ssl is false or absent (comments removed). An assume_ssl-only setup still fails because cookies are not marked Secure.",
262 Fix: "Set config.force_ssl = true in config/environments/production.rb (and config.assume_ssl = true behind a TLS-terminating proxy), then restart the application.",
263 Verify: "grep -n force_ssl config/environments/production.rb shows true, and curl -I http://<site>/ returns a 301 to https.",
264 Refs: []check.Ref{refRailsConfig, refRailsSecurity},
265 Standards: check.Standards{CWE: []string{"CWE-319", "CWE-614"}, OWASP: []string{"A02:2021", "A05:2021"}, ASVS: []string{"9.1.1", "3.4.1"}, WSTG: []string{"WSTG-CRYP-03"}, NIST: []string{"SC-8", "CM-6"}},
266 Run: func(ctx context.Context, s sys.System) []check.Result {
267 paths := collect.ProjectFiles(ctx, s, "config/environments/production.rb")
268 forceOn := regexp.MustCompile(`(?m)^\s*config\.force_ssl\s*=\s*true\b`)
269 rubyComment := regexp.MustCompile(`(?m)#[^\n]*`)
270 return perFile(paths, func(p string) []string {
271 r, err := sys.ReadFileNoFollow(ctx, s, p)
272 if err != nil {
273 return []string{unreadable}
274 }
275 if !forceOn.MatchString(rubyComment.ReplaceAllString(string(r.Data), "")) {
276 return []string{"config.force_ssl is not true"}
277 }
278 return nil
279 }, "Rails production does not force HTTPS", "No Rails production configuration found")
280 },
281 })
282
283 check.Register(&check.Check{
284 ID: "WEB-APP-006", Title: "Spring Boot Actuator Endpoints Fully Exposed", Domain: check.DomainWeb,
285 Severity: check.High, Category: check.CatExposure, Effort: check.EffortLow,
286 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
287 Summary: "Checks Spring Boot application properties for management.endpoints.web.exposure.include=* and probes the local site for an open /actuator/env.",
288 Rationale: "Actuator's env, configprops, heapdump and threaddump endpoints print environment variables, configuration (including credentials) and process memory. Exposing them all over HTTP without authentication is one of the most common Java application leaks.",
289 Detection: "Reads application.properties, application.yml and their -prod variants in the project folders for exposure.include set to * (or a list containing env, heapdump or configprops) with no management.server.port moving them off the public port. Also requests /actuator/env on each local site and fails on a JSON reply containing propertySources.",
290 Fix: "Set management.endpoints.web.exposure.include=health,info (and exclude env,heapdump,configprops), move management.server.port to a port that is not published, and require authentication for every actuator endpoint with Spring Security.",
291 Verify: "curl -s -o /dev/null -w '%{http_code}' https://<site>/actuator/env prints 401, 403 or 404.",
292 Refs: []check.Ref{refSpringAct, {Title: "Spring Boot documentation, Securing HTTP endpoints", URL: "https://docs.spring.io/spring-boot/reference/actuator/endpoints.html#actuator.endpoints.security"}, refWSTGConf02},
293 Standards: check.Standards{CWE: []string{"CWE-306", "CWE-497"}, OWASP: []string{"A05:2021", "A01:2021"}, ASVS: []string{"14.3.2", "4.3.2"}, WSTG: []string{"WSTG-CONF-02", "WSTG-CONF-05"}, NIST: []string{"CM-7", "AC-3", "SI-11"}},
294 Run: func(ctx context.Context, s sys.System) []check.Result {
295 paths := collect.ProjectFiles(ctx, s, "application.properties", "application.yml", "application.yaml",
296 "application-prod.properties", "application-prod.yml", "config/application.properties", "config/application.yml")
297 propAll := regexp.MustCompile(`(?m)^\s*management\.endpoints\.web\.exposure\.include\s*[=:]\s*["']?(\*|[^\n]*\b(env|heapdump|configprops)\b)`)
298 yamlAll := regexp.MustCompile(`(?s)exposure:\s*\n\s*include:\s*["']?(\*|[^\n]*\b(env|heapdump|configprops)\b)`)
299 mgmtPort := regexp.MustCompile(`(?m)^\s*management\.server\.port\s*[=:]|(?s)management:\s*\n(\s+.*\n)*?\s+server:\s*\n\s+port:`)
300 out := perFile(paths, func(p string) []string {
301 r, err := sys.ReadFileNoFollow(ctx, s, p)
302 if err != nil {
303 return []string{unreadable}
304 }
305 t := string(r.Data)
306 if (propAll.MatchString(t) || yamlAll.MatchString(t)) && !mgmtPort.MatchString(t) {
307 return []string{"management.endpoints.web.exposure.include exposes sensitive actuator endpoints on the application port"}
308 }
309 return nil
310 }, "Spring Boot actuator endpoints are fully exposed", "No Spring Boot application properties found")
311 var sites []check.Result
312 for _, st := range Sites(ctx, s) {
313 r, err := st.Get(ctx, s, "/actuator/env")
314 if err == nil && r.Status == 200 && !HTML(r) && strings.Contains(r.Body, "propertySources") {
315 sites = append(sites, check.FailR(st.Name, "/actuator/env answers without authentication"))
316 }
317 }
318 if len(sites) > 0 {
319 if len(out) == 1 && out[0].Status == check.Skip {
320 return sites
321 }
322 return append(out, sites...)
323 }
324 return out
325 },
326 })
327
328 check.Register(&check.Check{
329 ID: "WEB-APP-007", Title: "Flask Debug Mode Enabled in Application Code", Domain: check.DomainWeb,
330 Severity: check.Low, Category: check.CatConfig, Effort: check.EffortLow,
331 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
332 Summary: "Looks for app.run(debug=True) or app.debug = True in Flask entry files. A heuristic; the service check WEB-PY-004 covers the environment switches.",
333 Rationale: "Debug mode in code follows the application wherever it is deployed. If the file is ever run directly instead of through a WSGI server, the Werkzeug debugger and its code console come up for every visitor.",
334 Detection: "Reads app.py, wsgi.py, run.py, main.py and server.py in the project folders (files that import flask) and flags debug=True inside app.run(...) or an app.debug = True assignment, ignoring commented lines. Rated Low because a production WSGI server ignores app.run().",
335 Fix: "Remove debug=True from app.run() and read it from the environment for local work only (app.run(debug=os.environ.get('FLASK_DEBUG') == '1')); serve production with Gunicorn or uWSGI, which never call app.run().",
336 Verify: "grep -n 'debug' app.py shows no hard-coded True.",
337 Refs: []check.Ref{refFlaskDebug, {Title: "Flask documentation, Deploying to Production", URL: "https://flask.palletsprojects.com/en/stable/deploying/"}},
338 Standards: check.Standards{CWE: []string{"CWE-489", "CWE-215"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.3.2"}, WSTG: []string{"WSTG-CONF-02"}, NIST: []string{"CM-6", "CM-7"}},
339 Run: func(ctx context.Context, s sys.System) []check.Result {
340 var paths []string
341 texts := map[string]string{}
342 flaskImport := regexp.MustCompile(`(?m)^\s*(from\s+flask\s+import|import\s+flask)`)
343 for _, p := range collect.ProjectFiles(ctx, s, "app.py", "wsgi.py", "run.py", "main.py", "server.py") {
344 r, err := sys.ReadFileNoFollow(ctx, s, p)
345 if err != nil || !flaskImport.Match(r.Data) {
346 continue
347 }
348 paths = append(paths, p)
349 texts[p] = string(r.Data)
350 }
351 sort.Strings(paths)
352 pyComment := regexp.MustCompile(`(?m)#[^\n]*`)
353 runDebug := regexp.MustCompile(`\.run\s*\([^)]*\bdebug\s*=\s*True\b`)
354 attrDebug := regexp.MustCompile(`(?m)^\s*\w+\.debug\s*=\s*True\b`)
355 return perFile(paths, func(p string) []string {
356 t := pyComment.ReplaceAllString(texts[p], "")
357 var bad []string
358 if runDebug.MatchString(t) {
359 bad = append(bad, "app.run(debug=True)")
360 }
361 if attrDebug.MatchString(t) {
362 bad = append(bad, "app.debug = True")
363 }
364 return bad
365 }, "Flask debug mode is enabled in code", "No Flask entry files found")
366 },
367 })
368
369 check.Register(&check.Check{
370 ID: "WEB-APP-008", Title: "Dependency Lock Files Older Than a Year", Domain: check.DomainWeb,
371 Severity: check.Info, Category: check.CatPatch, Effort: check.EffortMedium,
372 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N",
373 Summary: "Reports composer.lock, package-lock.json, yarn.lock, Gemfile.lock and poetry.lock files not modified for 365 days or more (dependency freshness). Informational.",
374 Rationale: "A lock file pins exact library versions. One untouched for a year means the application has had no dependency updates in that time, so every library vulnerability published since is still present. The check cannot tell whether a specific vulnerability applies; the known-vulnerability checks and composer audit or npm audit do that.",
375 Detection: "Stats each lock file found in the project folders (metadata only) and reports those whose modification time is 365 days or older. Files whose time cannot be read are reported as incomplete.",
376 Fix: "Run composer update / npm update / bundle update / poetry update in a test environment, run the audit command of each ecosystem (composer audit, npm audit, bundle audit, pip-audit) and deploy. Schedule dependency updates at least quarterly.",
377 Verify: "stat -c '%y %n' composer.lock package-lock.json shows a recent date.",
378 Refs: []check.Ref{refOWASPDepCheck, {Title: "OWASP Top 10 2021, A06 Vulnerable and Outdated Components", URL: "https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components/"}},
379 Standards: check.Standards{CWE: []string{"CWE-1104", "CWE-1395"}, OWASP: []string{"A06:2021"}, ASVS: []string{"14.2.1"}, NIST: []string{"SI-2", "SA-22"}},
380 Run: func(ctx context.Context, s sys.System) []check.Result {
381 paths := collect.ProjectFiles(ctx, s, "composer.lock", "package-lock.json", "yarn.lock", "Gemfile.lock", "poetry.lock")
382 if len(paths) == 0 {
383 return []check.Result{check.SkipR("projects", "No dependency lock files found in the project folders")}
384 }
385 cutoff := time.Now().Add(-365 * 24 * time.Hour)
386 var out []check.Result
387 for _, p := range paths {
388 st, err := s.Stat(p)
389 if err != nil || st.ModTime.IsZero() {
390 out = append(out, check.IncompleteR(p, "Modification time could not be read"))
391 continue
392 }
393 days := int(time.Since(st.ModTime).Hours() / 24)
394 if st.ModTime.Before(cutoff) {
395 out = append(out, check.FailR(p, fmt.Sprintf("Lock file last changed %d days ago", days)))
396 } else {
397 out = append(out, check.PassR(p, fmt.Sprintf("Lock file changed %d days ago", days)))
398 }
399 }
400 return out
401 },
402 })
403
404 check.Register(&check.Check{
405 ID: "WEB-APP-009", Title: "Git Repository Stored Inside a Web Root", Domain: check.DomainWeb,
406 Severity: check.Medium, Category: check.CatExposure, Effort: check.EffortLow,
407 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
408 Summary: "Looks on disk for a .git folder directly inside each web document root.",
409 Rationale: "A .git folder in the document root is one web server rule away from being downloadable, which rebuilds the full source and its history, often with committed credentials. WEB-EXP-001 tests whether it is already reachable; this check finds it before a configuration change exposes it.",
410 Detection: "Stats <docroot>/.git/HEAD for each local document root (metadata only). Fails when it exists, even if the web server currently blocks dot-folders.",
411 Fix: "Deploy from a build step that copies only the needed files (git archive, rsync --exclude .git, or a CI artefact), or point the document root at a public/ sub-folder as Laravel, Symfony and Rails expect. Remove the .git folder from the web root and rotate any secret ever committed.",
412 Verify: "ls -d <docroot>/.git prints nothing.",
413 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/"}, refSecretsCS},
414 Standards: 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"}},
415 Run: func(ctx context.Context, s sys.System) []check.Result {
416 roots := collect.DocRoots(ctx, s)
417 if len(roots) == 0 {
418 return []check.Result{check.SkipR("web roots", "No document roots found in the local web server configuration")}
419 }
420 var out []check.Result
421 for _, root := range roots {
422 if _, err := s.Stat(root + "/.git/HEAD"); err == nil {
423 out = append(out, check.FailR(root, "A .git repository is stored inside the web root", root+"/.git"))
424 } else {
425 out = append(out, check.PassR(root, "No .git folder in the web root"))
426 }
427 }
428 return out
429 },
430 })
431
432 check.Register(&check.Check{
433 ID: "WEB-APP-010", Title: "Framework Writable Folders Open to All Users", Domain: check.DomainWeb,
434 Severity: check.Medium, Category: check.CatAccess, Effort: check.EffortLow,
435 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L",
436 Summary: "Checks Laravel and Symfony cache, log and storage folders for world-write permission.",
437 Rationale: "chmod -R 777 storage is a common fix for permission errors. Laravel's bootstrap/cache and Symfony's var/cache hold compiled PHP that the framework includes on every request, so any local user who can write there runs code as the web server. Logs and sessions in storage are readable and forgeable too.",
438 Detection: "For each Laravel (.env with APP_KEY) or Symfony (.env with APP_SECRET) project, stats storage, storage/logs, storage/framework, bootstrap/cache, var/cache and var/log next to the .env file (metadata only) and fails when any has the 'other write' bit set.",
439 Fix: "chmod -R o-w storage bootstrap/cache var and give the web server group write access instead: chgrp -R www-data storage bootstrap/cache && chmod -R g+w storage bootstrap/cache.",
440 Verify: "find storage bootstrap/cache var -perm -0002 -type d prints nothing.",
441 Refs: []check.Ref{refLaravelDeploy, {Title: "Symfony documentation, Setting up or Fixing File Permissions", URL: "https://symfony.com/doc/current/setup/file_permissions.html"}, {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/"}},
442 Standards: check.Standards{CWE: []string{"CWE-732", "CWE-94"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.1.3"}, WSTG: []string{"WSTG-CONF-09"}, NIST: []string{"AC-3", "AC-6", "CM-5"}},
443 Run: func(ctx context.Context, s sys.System) []check.Result {
444 files := envFiles(ctx, s, "APP_KEY", "APP_SECRET")
445 var paths []string
446 for _, f := range files {
447 paths = append(paths, f.path)
448 }
449 return perFile(paths, func(p string) []string {
450 dir := filepath.Dir(p)
451 var bad []string
452 for _, sub := range []string{"storage", "storage/logs", "storage/framework", "bootstrap/cache", "var/cache", "var/log"} {
453 if st, err := s.Stat(dir + "/" + sub); err == nil && st.Mode.Perm()&0o002 != 0 {
454 bad = append(bad, fmt.Sprintf("%04o %s", st.Mode.Perm(), dir+"/"+sub))
455 }
456 }
457 return bad
458 }, "Framework cache or storage folders are world-writable", "No Laravel or Symfony projects found")
459 },
460 })
461}

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-APP-001 Laravel Debug Mode On or Application Key Missing (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/WEB-APP-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_app_001,
  author       = {{OpenVAPT}},
  title        = {{WEB-APP-001 Laravel Debug Mode On or Application Key Missing}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/WEB-APP-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.