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
CriticalWEB-PY-004Secure ConfigurationWeb Apps

Python Interactive Debugger Enabled in a Service

Checks whether a service runs Flask or Werkzeug with the interactive debugger enabled.

Why It Matters
The Werkzeug debugger lets a visitor run Python code from an error page. Its PIN has been bypassed and brute-forced in practice, and with WERKZEUG_DEBUG_PIN=off there is no protection at all: this is remote code execution.
How It Detects
Reads ExecStart= and Environment= of long-running services and flags FLASK_DEBUG=1, FLASK_ENV=development, flask run --debug and WERKZEUG_DEBUG_PIN=off.
How to Fix
Remove the debug switches, run the application under a production WSGI server (Gunicorn, uWSGI or mod_wsgi) behind nginx or Apache, run systemctl daemon-reload and restart the service.
How to Verify
systemctl show -p ExecStart,Environment <unit> shows no debug switches.
CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Control Category
Secure Configuration: CIS Controls v8 4; NIST CSF 2.0 PR.PS
Fix Effort
Low
CIS Controls v8 Safeguard
16.7
NIST CSF 2.0 Subcategory
PR.PS-01
CWE
CWE-489, CWE-94
OWASP Top 10
A05:2021
OWASP ASVS 4.0.3
14.3.2
NIST SP 800-53 Rev. 5
CM-7, CM-6
References
Werkzeug documentation, Debugging Applications
Flask documentation, Debug mode
Learn the theory behind this check: P13 Application Servers: Tomcat, Node.js and Python.

Source Code

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

internal/checks/web/runtimes.go · Lines 1-360View on GitHub at This Commit
1package web
2
3import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "regexp"
8 "sort"
9 "strconv"
10 "strings"
11 "time"
12
13 "github.com/openvapt/openvapt/internal/check"
14 "github.com/openvapt/openvapt/internal/checks/collect"
15 "github.com/openvapt/openvapt/internal/sys"
16)
17
18// Node.js and Python application checks. They read systemd service units
19// (how the application is started), the Node.js version, and Django settings
20// files. Settings files can hold secrets, so only file paths and setting
21// names are reported, never values.
22
23// nodeEOL is the end-of-life date of each Node.js major release, from the
24// Node.js release schedule (https://github.com/nodejs/Release). Odd majors
25// are short-lived "current" releases.
26var nodeEOL = map[int]string{
27 12: "2022-04-30", 14: "2023-04-30", 16: "2023-09-11", 17: "2022-06-01", 18: "2025-04-30", 19: "2023-06-01",
28 20: "2026-04-30", 21: "2024-06-01", 22: "2027-04-30", 23: "2025-06-01", 24: "2028-04-30", 25: "2026-06-01",
29 26: "2029-04-30",
30}
31
32var nodeVersion = regexp.MustCompile(`v?(\d+)\.(\d+)\.(\d+)`)
33
34// unitCommand joins a unit's ExecStart= lines and Environment= values so
35// that command-line flags and environment switches can be searched together.
36func unitCommand(u collect.Unit) string {
37 return strings.Join(u.All("service.execstart"), " ") + " " + strings.Join(u.All("service.environment"), " ")
38}
39
40var nodeRunner = regexp.MustCompile(`(^|[/\s])(node|nodejs|npm|npx|yarn|pnpm|pm2-runtime|pm2)(\s|$)`)
41
42func runsNode(u collect.Unit) bool { return nodeRunner.MatchString(u.Get("service.execstart")) }
43
44// unitResults runs fn over long-running service units, or skips if fn
45// selects none.
46func unitResults(ctx context.Context, s sys.System, applies func(collect.Unit) bool, judge func(collect.Unit) string, okMsg, failMsg string) []check.Result {
47 var n int
48 var bad []string
49 for _, u := range collect.Units(ctx, s) {
50 if !u.LongRunning() || !applies(u) {
51 continue
52 }
53 n++
54 if why := judge(u); why != "" {
55 bad = append(bad, u.Name+": "+why)
56 }
57 }
58 if n == 0 {
59 return []check.Result{check.SkipR("systemd units", "No matching application services were found")}
60 }
61 if len(bad) > 0 {
62 return []check.Result{check.FailR("systemd units", failMsg, bad...)}
63 }
64 return []check.Result{check.PassR("systemd units", okMsg)}
65}
66
67// --inspect-port only sets a port; it does not start the inspector.
68var inspectFlag = regexp.MustCompile(`--inspect(-brk|-wait)?(=(\S+))?(\s|$)`)
69var pyDevServer = regexp.MustCompile(`manage\.py\s+runserver|\bflask\s+run\b|-m\s+(http\.server|SimpleHTTPServer)\b|\buvicorn\b.*--reload|\bstreamlit\s+run\b`)
70var flaskDebug = regexp.MustCompile(`(?i)\bFLASK_DEBUG=(1|true|yes|on)\b|\bFLASK_ENV=development\b|\bflask\b.*\s--debug\b|\bWERKZEUG_DEBUG_PIN=off\b`)
71
72// django settings patterns; DEBUG/ALLOWED_HOSTS/SECRET_KEY assignments at
73// the start of a line, as Django's own generated settings write them.
74var (
75 djDebugOn = regexp.MustCompile(`(?m)^\s*DEBUG\s*=\s*True\b`)
76 djDebugSafe = regexp.MustCompile(`^(False\b|os\.|env|config\(|bool\()`)
77 djDebugAny = regexp.MustCompile(`(?m)^\s*DEBUG\s*=\s*(.*)$`)
78 djHostsAny = regexp.MustCompile(`(?m)^\s*ALLOWED_HOSTS\s*=\s*[\[(]\s*['"]\*['"]`)
79 djInsecure = regexp.MustCompile(`(?m)^\s*SECRET_KEY\s*=\s*['"]django-insecure-`)
80 djMarker = regexp.MustCompile(`INSTALLED_APPS|ROOT_URLCONF|DJANGO_SETTINGS_MODULE|django`)
81)
82
83type djangoFile struct{ path, text string }
84
85// djangoSettings returns Django settings files grouped by folder.
86func djangoSettings(ctx context.Context, s sys.System) map[string][]djangoFile {
87 out := map[string][]djangoFile{}
88 for _, p := range collect.ProjectFiles(ctx, s, "settings.py", "settings/*.py") {
89 // Project folders are writable by site users, who could replace
90 // settings.py with a link to /etc/shadow, so links are refused.
91 r, err := sys.ReadFileNoFollow(ctx, s, p)
92 if err != nil || !djMarker.Match(r.Data) && !djDebugOn.Match(r.Data) {
93 continue
94 }
95 dir := filepath.Dir(p)
96 out[dir] = append(out[dir], djangoFile{p, string(r.Data)})
97 }
98 return out
99}
100
101func djangoCheck(ctx context.Context, s sys.System, judge func(dir []djangoFile) []string, failMsg string) []check.Result {
102 dirs := djangoSettings(ctx, s)
103 if len(dirs) == 0 {
104 return []check.Result{check.SkipR("django", "No Django settings files found")}
105 }
106 keys := make([]string, 0, len(dirs))
107 for k := range dirs {
108 keys = append(keys, k)
109 }
110 sort.Strings(keys)
111 var out []check.Result
112 for _, k := range keys {
113 if bad := judge(dirs[k]); len(bad) > 0 {
114 out = append(out, check.FailR(k, failMsg, bad...))
115 } else {
116 out = append(out, check.PassR(k, "No issue found"))
117 }
118 }
119 return out
120}
121
122func matching(fs []djangoFile, re *regexp.Regexp, what string) []string {
123 var out []string
124 for _, f := range fs {
125 if re.MatchString(f.text) {
126 out = append(out, f.path+": "+what)
127 }
128 }
129 return out
130}
131
132var refDjangoDeploy = check.Ref{Title: "Django documentation, Deployment checklist", URL: "https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/"}
133var refNodeSec = check.Ref{Title: "OWASP Node.js Security Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html"}
134
135func init() {
136 check.Register(&check.Check{
137 ID: "WEB-NODE-001", Title: "Node.js Version Past End of Life", Domain: check.DomainWeb,
138 Severity: check.High, Category: check.CatPatch, Effort: check.EffortHigh,
139 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
140 Summary: "Checks whether the installed Node.js runtime is a release line that no longer receives security fixes.",
141 Rationale: "After end of life the Node.js project publishes no more security releases, so every later vulnerability in the runtime, its bundled OpenSSL or its HTTP parser stays open for good.",
142 Detection: "Runs node --version (or nodejs --version), a read-only command, and compares the major version with the Node.js release schedule's end-of-life dates.",
143 Fix: "Move to an Active or Maintenance LTS release (even-numbered majors), for example with the NodeSource or distribution packages, test the application, then restart its service.",
144 Verify: "node --version shows a supported LTS major.",
145 Refs: []check.Ref{{Title: "Node.js Release Working Group, release schedule", URL: "https://github.com/nodejs/Release"}, {Title: "endoflife.date, Node.js (community-maintained lifecycle data)", URL: "https://endoflife.date/nodejs"}},
146 Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, NIST: []string{"SI-2", "SA-22"}, CISTopic: "Use supported software"},
147 Run: func(ctx context.Context, s sys.System) []check.Result {
148 out, err := s.Run(ctx, "node", "--version")
149 if err != nil {
150 out, err = s.Run(ctx, "nodejs", "--version")
151 }
152 if err != nil {
153 return []check.Result{check.SkipR("node", "Node.js is not installed")}
154 }
155 m := nodeVersion.FindStringSubmatch(out)
156 if m == nil {
157 return []check.Result{check.IncompleteR("node", "Could not read the Node.js version")}
158 }
159 major, _ := strconv.Atoi(m[1])
160 eol, ok := nodeEOL[major]
161 if !ok {
162 if major < 12 {
163 return []check.Result{check.FailR("node", "Node.js "+m[0]+" is long past end of life")}
164 }
165 return []check.Result{check.PassR("node", "Node.js "+m[0]+" is newer than the built-in schedule")}
166 }
167 if eol <= time.Now().UTC().Format("2006-01-02") {
168 return []check.Result{check.FailR("node", fmt.Sprintf("Node.js %s reached end of life on %s", m[0], eol))}
169 }
170 return []check.Result{check.PassR("node", fmt.Sprintf("Node.js %s is supported until %s", m[0], eol))}
171 },
172 })
173
174 check.Register(&check.Check{
175 ID: "WEB-NODE-002", Title: "Node.js Debugger (Inspector) Enabled in a Service", Domain: check.DomainWeb,
176 Severity: check.Critical, Category: check.CatConfig, Effort: check.EffortLow,
177 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
178 Summary: "Checks whether a Node.js service is started with --inspect, which opens the V8 debugger.",
179 Rationale: "The inspector has no authentication: anyone who can connect to it can run any JavaScript inside the application, as the service's user. Bound to 0.0.0.0 that is remote code execution; even on 127.0.0.1 any local user or server-side request forgery bug can reach it.",
180 Detection: "Reads ExecStart= and Environment= (including NODE_OPTIONS) of long-running services that run node, npm, yarn, pnpm or pm2, and flags --inspect, --inspect-brk and --inspect-wait. A network-reachable listener on port 9229 is also reported by NET-APP-001.",
181 Fix: "Remove --inspect from the service and from NODE_OPTIONS, run systemctl daemon-reload and restart. Debug on a development machine, or temporarily over an SSH tunnel to 127.0.0.1:9229.",
182 Verify: "systemctl show -p ExecStart,Environment <unit> | grep -c inspect prints 0, and nothing listens on 9229.",
183 Refs: []check.Ref{{Title: "Node.js documentation, Debugging guide (security implications)", URL: "https://nodejs.org/learn/getting-started/debugging"}, refNodeSec},
184 Standards: check.Standards{CWE: []string{"CWE-489", "CWE-306"}, OWASP: []string{"A05:2021"}, NIST: []string{"CM-7", "CM-6"}},
185 Run: func(ctx context.Context, s sys.System) []check.Result {
186 return unitResults(ctx, s, runsNode, func(u collect.Unit) string {
187 m := inspectFlag.FindStringSubmatch(unitCommand(u))
188 if m == nil {
189 return ""
190 }
191 if m[3] != "" && !strings.HasPrefix(m[3], "127.") && !strings.HasPrefix(m[3], "localhost") && strings.Contains(m[3], ":") {
192 return "starts the inspector on a network address (" + m[0] + ")"
193 }
194 return "starts the inspector (" + m[0] + ")"
195 }, "No Node.js service starts the inspector", "Node.js services start the debugger")
196 },
197 })
198
199 check.Register(&check.Check{
200 ID: "WEB-NODE-003", Title: "Node.js Service Not Running in Production Mode", Domain: check.DomainWeb,
201 Severity: check.Low, Category: check.CatConfig, Effort: check.EffortLow,
202 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
203 Summary: "Checks whether Node.js services set NODE_ENV=production.",
204 Rationale: "Express and many other frameworks treat a missing NODE_ENV as development: error pages then include stack traces with file paths, and caching and other production safeguards are off.",
205 Detection: "For long-running services whose ExecStart= runs node, npm, yarn, pnpm or pm2, reads Environment= and fails when NODE_ENV is missing or not production. Variables loaded with EnvironmentFile= are read only for NODE_ENV.",
206 Fix: "Add Environment=NODE_ENV=production to the [Service] section (systemctl edit <unit>), run systemctl daemon-reload and restart the service.",
207 Verify: "systemctl show -p Environment <unit> includes NODE_ENV=production.",
208 Refs: []check.Ref{{Title: "Express documentation, Production best practices: performance and reliability", URL: "https://expressjs.com/en/advanced/best-practice-performance/"}, refNodeSec},
209 Standards: check.Standards{CWE: []string{"CWE-489", "CWE-209"}, OWASP: []string{"A05:2021"}, NIST: []string{"CM-6"}},
210 Run: func(ctx context.Context, s sys.System) []check.Result {
211 return unitResults(ctx, s, runsNode, func(u collect.Unit) string {
212 env := u.Env()
213 v, ok := env["NODE_ENV"]
214 if !ok {
215 v, ok = envFileValue(ctx, s, u, "NODE_ENV")
216 }
217 switch {
218 case !ok:
219 return "NODE_ENV is not set"
220 case v != "production":
221 return "NODE_ENV=" + v
222 }
223 return ""
224 }, "Node.js services run with NODE_ENV=production", "Node.js services are not in production mode")
225 },
226 })
227
228 check.Register(&check.Check{
229 ID: "WEB-PY-001", Title: "Django Debug Mode Enabled", Domain: check.DomainWeb,
230 Severity: check.High, Category: check.CatExposure, Effort: check.EffortLow,
231 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
232 Summary: "Checks Django settings files for DEBUG = True with no production override.",
233 Rationale: "With DEBUG on, every error page shows source code, local variables, SQL queries and most settings, and Django keeps every SQL query in memory. Attackers trigger errors on purpose to read them.",
234 Detection: "Finds settings.py and settings/*.py files that look like Django settings in /srv, /opt, /var/www, home directories and web roots (three folders deep), and fails when a folder sets DEBUG = True and no file in the same folder sets DEBUG = False or reads it from the environment.",
235 Fix: "Set DEBUG = False in production, preferably from the environment (DEBUG = os.environ.get('DJANGO_DEBUG') == '1'), set ALLOWED_HOSTS, and restart the application. Run python manage.py check --deploy.",
236 Verify: "python manage.py check --deploy reports no DEBUG warning.",
237 Refs: []check.Ref{refDjangoDeploy, {Title: "Django settings reference, DEBUG", URL: "https://docs.djangoproject.com/en/6.1/ref/settings/#debug"}},
238 Standards: check.Standards{CWE: []string{"CWE-489", "CWE-215"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.3.2"}, WSTG: []string{"WSTG-ERRH-01"}, NIST: []string{"CM-6"}},
239 Run: func(ctx context.Context, s sys.System) []check.Result {
240 return djangoCheck(ctx, s, func(fs []djangoFile) []string {
241 // Python keeps the last assignment in a file. A folder is safe
242 // when some file's final DEBUG is False or read from the
243 // environment (a production module overriding a base one).
244 var on []string
245 for _, f := range fs {
246 all := djDebugAny.FindAllStringSubmatch(f.text, -1)
247 if len(all) == 0 {
248 continue
249 }
250 last := strings.TrimSpace(all[len(all)-1][1])
251 if djDebugSafe.MatchString(last) {
252 return nil
253 }
254 if strings.HasPrefix(last, "True") {
255 on = append(on, f.path+": DEBUG = True")
256 }
257 }
258 return on
259 }, "Django debug mode is on")
260 },
261 })
262
263 check.Register(&check.Check{
264 ID: "WEB-PY-002", Title: "Django Accepts Any Host Header", Domain: check.DomainWeb,
265 Severity: check.Medium, Category: check.CatWebApp, Effort: check.EffortLow,
266 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N",
267 Summary: "Checks Django settings for ALLOWED_HOSTS = ['*'].",
268 Rationale: "ALLOWED_HOSTS protects against Host header attacks. With a wildcard, an attacker can make Django build password-reset and other absolute links that point at the attacker's domain, and can poison shared caches.",
269 Detection: "Reads the same Django settings files as WEB-PY-001 and flags an ALLOWED_HOSTS list or tuple that starts with '*'.",
270 Fix: "List the real domain names: ALLOWED_HOSTS = ['example.com', 'www.example.com'], and restart the application.",
271 Verify: "python manage.py check --deploy",
272 Refs: []check.Ref{refDjangoDeploy, {Title: "Django settings reference, ALLOWED_HOSTS", URL: "https://docs.djangoproject.com/en/6.1/ref/settings/#allowed-hosts"}},
273 Standards: check.Standards{CWE: []string{"CWE-346"}, OWASP: []string{"A05:2021"}, WSTG: []string{"WSTG-INPV-17"}, NIST: []string{"CM-6", "SI-10"}},
274 Run: func(ctx context.Context, s sys.System) []check.Result {
275 return djangoCheck(ctx, s, func(fs []djangoFile) []string { return matching(fs, djHostsAny, "ALLOWED_HOSTS allows any host") }, "Django accepts any Host header")
276 },
277 })
278
279 check.Register(&check.Check{
280 ID: "WEB-PY-003", Title: "Django Uses the Generated Development Secret Key", Domain: check.DomainWeb,
281 Severity: check.High, Category: check.CatCrypto, Effort: check.EffortLow,
282 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
283 Summary: "Checks Django settings for a SECRET_KEY that starts with django-insecure-, the marker of the key generated by startproject.",
284 Rationale: "SECRET_KEY signs sessions, password-reset tokens and other data. The generated key sits in source files that are usually copied into repositories and backups; anyone who has it can forge signed data and, with some session backends, take over accounts.",
285 Detection: "Reads the same Django settings files as WEB-PY-001 and flags SECRET_KEY = 'django-insecure-...'. The key itself is never copied into the report.",
286 Fix: "Generate a new key (python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'), load it from the environment or a root-readable file (SECRET_KEY = os.environ['DJANGO_SECRET_KEY']), and restart. Changing the key logs all users out.",
287 Verify: "python manage.py check --deploy reports no SECRET_KEY warning.",
288 Refs: []check.Ref{refDjangoDeploy, {Title: "Django settings reference, SECRET_KEY", URL: "https://docs.djangoproject.com/en/6.1/ref/settings/#secret-key"}},
289 Standards: check.Standards{CWE: []string{"CWE-798", "CWE-321"}, OWASP: []string{"A02:2021", "A05:2021"}, ASVS: []string{"2.10.4"}, NIST: []string{"IA-5", "SC-12"}},
290 Run: func(ctx context.Context, s sys.System) []check.Result {
291 return djangoCheck(ctx, s, func(fs []djangoFile) []string {
292 return matching(fs, djInsecure, "uses the generated django-insecure SECRET_KEY")
293 }, "Django uses the generated development SECRET_KEY")
294 },
295 })
296
297 check.Register(&check.Check{
298 ID: "WEB-PY-004", Title: "Python Interactive Debugger Enabled in a Service", Domain: check.DomainWeb,
299 Severity: check.Critical, Category: check.CatConfig, Effort: check.EffortLow,
300 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
301 Summary: "Checks whether a service runs Flask or Werkzeug with the interactive debugger enabled.",
302 Rationale: "The Werkzeug debugger lets a visitor run Python code from an error page. Its PIN has been bypassed and brute-forced in practice, and with WERKZEUG_DEBUG_PIN=off there is no protection at all: this is remote code execution.",
303 Detection: "Reads ExecStart= and Environment= of long-running services and flags FLASK_DEBUG=1, FLASK_ENV=development, flask run --debug and WERKZEUG_DEBUG_PIN=off.",
304 Fix: "Remove the debug switches, run the application under a production WSGI server (Gunicorn, uWSGI or mod_wsgi) behind nginx or Apache, run systemctl daemon-reload and restart the service.",
305 Verify: "systemctl show -p ExecStart,Environment <unit> shows no debug switches.",
306 Refs: []check.Ref{{Title: "Werkzeug documentation, Debugging Applications", URL: "https://werkzeug.palletsprojects.com/en/stable/debug/"}, {Title: "Flask documentation, Debug mode", URL: "https://flask.palletsprojects.com/en/stable/quickstart/#debug-mode"}},
307 Standards: check.Standards{CWE: []string{"CWE-489", "CWE-94"}, OWASP: []string{"A05:2021"}, NIST: []string{"CM-7", "CM-6"}},
308 Run: func(ctx context.Context, s sys.System) []check.Result {
309 return unitResults(ctx, s, func(collect.Unit) bool { return true }, func(u collect.Unit) string {
310 if m := flaskDebug.FindString(unitCommand(u)); m != "" {
311 return "enables the debugger (" + m + ")"
312 }
313 return ""
314 }, "No service enables the Flask or Werkzeug debugger", "Services enable the Python interactive debugger")
315 },
316 })
317
318 check.Register(&check.Check{
319 ID: "WEB-PY-005", Title: "Python Development Server Used in a Service", Domain: check.DomainWeb,
320 Severity: check.Medium, Category: check.CatConfig, Effort: check.EffortMedium,
321 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L",
322 Summary: "Checks whether a service runs a development web server such as manage.py runserver, flask run or python -m http.server.",
323 Rationale: "Development servers are documented as unsuitable for production: they have not been security-audited, handle load and slow clients poorly and often run with debugging features. python -m http.server publishes a whole folder.",
324 Detection: "Reads ExecStart= of long-running services and flags manage.py runserver, flask run, python -m http.server or SimpleHTTPServer, uvicorn --reload and streamlit run.",
325 Fix: "Serve the application with a production server (Gunicorn or uWSGI for WSGI, Uvicorn without --reload or Hypercorn for ASGI) bound to 127.0.0.1, behind nginx or Apache. Serve static files from the web server.",
326 Verify: "systemctl show -p ExecStart <unit> shows a production server.",
327 Refs: []check.Ref{{Title: "Django documentation, django-admin runserver (not for production)", URL: "https://docs.djangoproject.com/en/6.1/ref/django-admin/#runserver"}, {Title: "Flask documentation, Deploying to Production", URL: "https://flask.palletsprojects.com/en/stable/deploying/"}},
328 Standards: check.Standards{CWE: []string{"CWE-489", "CWE-1188"}, OWASP: []string{"A05:2021"}, NIST: []string{"CM-7"}},
329 Run: func(ctx context.Context, s sys.System) []check.Result {
330 return unitResults(ctx, s, func(collect.Unit) bool { return true }, func(u collect.Unit) string {
331 if m := pyDevServer.FindString(u.Get("service.execstart")); m != "" {
332 return "runs a development server (" + m + ")"
333 }
334 return ""
335 }, "No service runs a Python development server", "Services run Python development servers")
336 },
337 })
338}
339
340// envFileValue reads one variable from a unit's EnvironmentFile= files. The
341// files may hold secrets, so only the requested variable is extracted.
342func envFileValue(ctx context.Context, s sys.System, u collect.Unit, name string) (string, bool) {
343 for _, p := range u.All("service.environmentfile") {
344 p = strings.TrimPrefix(strings.TrimSpace(p), "-")
345 if !strings.HasPrefix(p, "/") {
346 continue
347 }
348 r, err := s.ReadFile(ctx, p)
349 if err != nil {
350 continue
351 }
352 for _, line := range strings.Split(string(r.Data), "\n") {
353 k, v, ok := strings.Cut(strings.TrimPrefix(strings.TrimSpace(line), "export "), "=")
354 if ok && strings.TrimSpace(k) == name {
355 return strings.Trim(strings.TrimSpace(v), `"'`), true
356 }
357 }
358 }
359 return "", false
360}

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-PY-004 Python Interactive Debugger Enabled in a Service (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/WEB-PY-004.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_py_004,
  author       = {{OpenVAPT}},
  title        = {{WEB-PY-004 Python Interactive Debugger Enabled in a Service}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/WEB-PY-004.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.