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
MediumWEB-CMS-013Web Application SecurityWeb Apps

Drupal Trusted Host Patterns Not Set

Checks Drupal settings.php for the trusted_host_patterns setting.

Why It Matters
Without trusted_host_patterns Drupal builds absolute links, including password reset links, from whatever Host header the request carries. An attacker who sends a crafted Host header can poison caches and redirect reset links to a domain they control.
How It Detects
Reads each sites/*/settings.php found through the web roots (comments removed) and fails when $settings['trusted_host_patterns'] is not assigned.
How to Fix
Add to settings.php: $settings['trusted_host_patterns'] = ['^www\.example\.com$', '^example\.com$']; then clear caches (drush cr). The status report page should no longer warn about trusted hosts.
How to Verify
drush status-report | grep -i 'trusted host'
CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
Control Category
Web Application Security: CIS Controls v8 16; NIST CSF 2.0 PR.PS, PR.DS
Fix Effort
Low
CIS Controls v8 Safeguard
16.7
NIST CSF 2.0 Subcategory
PR.PS-01
OWASP WSTG v4.2
WSTG-INPV-17
CWE
CWE-644, CWE-346
OWASP Top 10
A05:2021
OWASP ASVS 4.0.3
14.4.1
NIST SP 800-53 Rev. 5
CM-6, SI-10
References
Drupal.org, Trusted Host settings
OWASP WSTG-INPV-17, Testing for Host Header Injection
Learn the theory behind this check: P6 Information Exposure and Misconfiguration, P14 CMS Hardening in Depth: WordPress, Joomla and Drupal, A6 Application Framework and CMS Configuration.

Source Code

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

internal/checks/web/cms_depth.go · Lines 1-298View on GitHub at This Commit
1package web
2
3import (
4 "context"
5 "regexp"
6 "strconv"
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// Deeper CMS checks (WEB-CMS-012 and later): Drupal and Joomla lifecycle and
15// settings, Magento and Ghost modes, and two more WordPress exposures. Files
16// are found through the local web roots and read without following
17// symlinks; only version numbers and setting names reach the report.
18
19var (
20 refDrupalSettings = check.Ref{Title: "Drupal.org, Trusted Host settings", URL: "https://www.drupal.org/docs/getting-started/installing-drupal/trusted-host-settings"}
21 refDrupalCoreEOL = check.Ref{Title: "Drupal.org, Drupal core release cycle and end of life", URL: "https://www.drupal.org/about/core/policies/core-release-cycles/schedule"}
22 refDrupalUpdate = check.Ref{Title: "Drupal.org, Running update.php (update_free_access)", URL: "https://www.drupal.org/docs/updating-drupal/update-core-via-drush-or-manually"}
23 refJoomlaEOL = check.Ref{Title: "Joomla Documentation, Joomla Release and Support Cycle", URL: "https://docs.joomla.org/Joomla!_CMS_versions"}
24 refMagentoModes = check.Ref{Title: "Adobe Commerce documentation, Set the Magento mode", URL: "https://experienceleague.adobe.com/en/docs/commerce-operations/configuration-guide/cli/set-mode"}
25 refGhostConfig = check.Ref{Title: "Ghost documentation, Configuration (url)", URL: "https://ghost.org/docs/config/"}
26 refWPCron = check.Ref{Title: "WordPress Developer Resources, Hooking WP-Cron Into the System Task Scheduler", URL: "https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/"}
27 refWPBrute = check.Ref{Title: "WordPress Developer Resources, Brute Force Attacks", URL: "https://developer.wordpress.org/advanced-administration/security/brute-force/"}
28)
29
30// drupalEOL is the end of security coverage for each Drupal major, from the
31// Drupal core release schedule. Majors not listed are treated as supported.
32var drupalEOL = map[int]string{7: "2025-01-05", 8: "2020-11-02", 9: "2023-11-01"}
33
34// joomlaEOL is the end of security support for each Joomla series.
35var joomlaEOL = map[string]string{"1.5": "2012-09-27", "1.6": "2011-08-19", "1.7": "2012-02-24", "2.5": "2014-12-31", "3": "2023-08-17", "4": "2025-10-17"}
36
37// noFollowText reads a file under a web root without following a symlink.
38func noFollowText(ctx context.Context, s sys.System, p string) (string, bool) {
39 r, err := sys.ReadFileNoFollow(ctx, s, p)
40 if err != nil || !r.Complete {
41 return "", false
42 }
43 return string(r.Data), true
44}
45
46// perRoot runs judge for every document root that has the CMS, or skips.
47// judge returns (result, true) when the CMS is present under root.
48func perRoot(ctx context.Context, s sys.System, judge func(root string) (check.Result, bool), skipMsg string) []check.Result {
49 var out []check.Result
50 for _, root := range collect.DocRoots(ctx, s) {
51 if r, ok := judge(root); ok {
52 out = append(out, r)
53 }
54 }
55 if len(out) == 0 {
56 return []check.Result{check.SkipR("web roots", skipMsg)}
57 }
58 return out
59}
60
61func init() {
62 check.Register(&check.Check{
63 ID: "WEB-CMS-012", Title: "Drupal Core Past End of Life", Domain: check.DomainWeb,
64 Severity: check.High, Category: check.CatPatch, Effort: check.EffortHigh,
65 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
66 Summary: "Reads the installed Drupal core version from disk and compares its major release with the Drupal security coverage dates.",
67 Rationale: "Drupal 7, 8 and 9 no longer receive security advisories from the Drupal Security Team. Drupalgeddon (CVE-2014-3704) and Drupalgeddon 2 (CVE-2018-7600) showed how quickly unpatched Drupal sites are taken over at scale.",
68 Detection: "Reads core/lib/Drupal.php (const VERSION) or includes/bootstrap.inc (define('VERSION')) under each local web root and fails when the major release's end-of-life date has passed: 7 (2025-01-05), 8 (2020-11-02), 9 (2023-11-01).",
69 Fix: "Upgrade to a supported Drupal major with composer (drupal/core-recommended), run drush updatedb and test the site; for Drupal 7, migrate with the Migrate module or commission a rebuild.",
70 Verify: "drush status --field=drupal-version or grep VERSION core/lib/Drupal.php",
71 Refs: []check.Ref{refDrupalCoreEOL, {Title: "NVD, CVE-2018-7600", URL: "https://nvd.nist.gov/vuln/detail/cve-2018-7600"}},
72 Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, ASVS: []string{"14.2.1"}, NIST: []string{"SI-2", "SA-22"}},
73 Run: func(ctx context.Context, s sys.System) []check.Result {
74 d8 := regexp.MustCompile(`const VERSION = '(\d+)\.[\d.]+[^']*'`)
75 d7 := regexp.MustCompile(`define\('VERSION', '(\d+)\.[\d.]+[^']*'\)`)
76 return perRoot(ctx, s, func(root string) (check.Result, bool) {
77 var m []string
78 if t, ok := noFollowText(ctx, s, root+"/core/lib/Drupal.php"); ok {
79 m = d8.FindStringSubmatch(t)
80 } else if t, ok := noFollowText(ctx, s, root+"/includes/bootstrap.inc"); ok {
81 m = d7.FindStringSubmatch(t)
82 } else {
83 return check.Result{}, false
84 }
85 if m == nil {
86 return check.IncompleteR(root, "Drupal is installed but its version line was not recognised"), true
87 }
88 major, _ := strconv.Atoi(m[1])
89 eol, known := drupalEOL[major]
90 if known && eol <= today() {
91 return check.FailR(root, "Drupal "+m[1]+" reached end of life on "+eol), true
92 }
93 if !known && major < 7 {
94 return check.FailR(root, "Drupal "+m[1]+" is older than every supported release"), true
95 }
96 return check.PassR(root, "Drupal "+m[1]+" is a supported major release"), true
97 }, "No Drupal installation found in the local web roots")
98 },
99 })
100
101 check.Register(&check.Check{
102 ID: "WEB-CMS-013", Title: "Drupal Trusted Host Patterns Not Set", Domain: check.DomainWeb,
103 Severity: check.Medium, Category: check.CatWebApp, Effort: check.EffortLow,
104 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N",
105 Summary: "Checks Drupal settings.php for the trusted_host_patterns setting.",
106 Rationale: "Without trusted_host_patterns Drupal builds absolute links, including password reset links, from whatever Host header the request carries. An attacker who sends a crafted Host header can poison caches and redirect reset links to a domain they control.",
107 Detection: "Reads each sites/*/settings.php found through the web roots (comments removed) and fails when $settings['trusted_host_patterns'] is not assigned.",
108 Fix: "Add to settings.php: $settings['trusted_host_patterns'] = ['^www\\.example\\.com$', '^example\\.com$']; then clear caches (drush cr). The status report page should no longer warn about trusted hosts.",
109 Verify: "drush status-report | grep -i 'trusted host'",
110 Refs: []check.Ref{refDrupalSettings, {Title: "OWASP WSTG-INPV-17, Testing for Host Header Injection", URL: "https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/07-Input_Validation_Testing/17-Testing_for_Host_Header_Injection/"}},
111 Standards: check.Standards{CWE: []string{"CWE-644", "CWE-346"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.4.1"}, WSTG: []string{"WSTG-INPV-17"}, NIST: []string{"CM-6", "SI-10"}},
112 Run: func(ctx context.Context, s sys.System) []check.Result {
113 trusted := regexp.MustCompile(`\$settings\[\s*['"]trusted_host_patterns['"]\s*\]\s*=`)
114 return cmsCheck(ctx, s, "Drupal", func(c collect.CMSConfig) []string {
115 if !trusted.MatchString(phpComment.ReplaceAllString(c.Text, "$1")) {
116 return []string{"$settings['trusted_host_patterns'] is not set"}
117 }
118 return nil
119 }, "Drupal accepts any Host header")
120 },
121 })
122
123 check.Register(&check.Check{
124 ID: "WEB-CMS-014", Title: "Joomla Core Past End of Life", Domain: check.DomainWeb,
125 Severity: check.High, Category: check.CatPatch, Effort: check.EffortHigh,
126 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
127 Summary: "Reads the installed Joomla version from administrator/manifests/files/joomla.xml on disk and compares it with the Joomla support dates.",
128 Rationale: "Joomla 3 left security support in August 2023 and Joomla 4 in October 2025. Sites on those series receive no fixes for new core vulnerabilities, while the exact version remains easy to fingerprint.",
129 Detection: "Reads <version> from administrator/manifests/files/joomla.xml under each local web root and fails when the series' end-of-support date has passed (1.5, 1.6, 1.7, 2.5, 3.x, 4.x). Joomla 5 and later pass.",
130 Fix: "Update to the current Joomla series through the Joomla Update component (back up first, check extension compatibility, then update), and keep the one-click updater enabled.",
131 Verify: "grep '<version>' administrator/manifests/files/joomla.xml",
132 Refs: []check.Ref{refJoomlaEOL, {Title: "Joomla Documentation, Security Checklist", URL: "https://docs.joomla.org/Security_Checklist"}},
133 Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, ASVS: []string{"14.2.1"}, NIST: []string{"SI-2", "SA-22"}},
134 Run: func(ctx context.Context, s sys.System) []check.Result {
135 ver := regexp.MustCompile(`<version>\s*(\d+)\.(\d+)[^<]*</version>`)
136 return perRoot(ctx, s, func(root string) (check.Result, bool) {
137 t, ok := noFollowText(ctx, s, root+"/administrator/manifests/files/joomla.xml")
138 if !ok {
139 return check.Result{}, false
140 }
141 m := ver.FindStringSubmatch(collect.StripXMLComments(t))
142 if m == nil {
143 return check.IncompleteR(root, "Joomla manifest found but no version element"), true
144 }
145 v := m[1] + "." + m[2]
146 eol, known := joomlaEOL[v]
147 if !known {
148 eol, known = joomlaEOL[m[1]]
149 }
150 if known && eol <= today() {
151 return check.FailR(root, "Joomla "+v+" left security support on "+eol), true
152 }
153 return check.PassR(root, "Joomla "+v+" is a supported series"), true
154 }, "No Joomla installation found in the local web roots")
155 },
156 })
157
158 check.Register(&check.Check{
159 ID: "WEB-CMS-015", Title: "Magento Not in Production Mode", Domain: check.DomainWeb,
160 Severity: check.Medium, Category: check.CatExposure, Effort: check.EffortLow,
161 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
162 Summary: "Checks app/etc/env.php for a MAGE_MODE other than production.",
163 Rationale: "Developer and default modes show exceptions with stack traces and file paths to shoppers, disable static file signing and compile code on the fly. Adobe documents production as the only mode for a live store.",
164 Detection: "Reads app/etc/env.php in each local web root and its parent folder (Magento serves from pub/) and fails when 'MAGE_MODE' is developer or default.",
165 Fix: "bin/magento deploy:mode:set production (which also deploys static content and compiles DI), then clear caches. Keep developer mode for staging only.",
166 Verify: "bin/magento deploy:mode:show prints production.",
167 Refs: []check.Ref{refMagentoModes, {Title: "Adobe Commerce documentation, Security best practices", URL: "https://experienceleague.adobe.com/en/docs/commerce-admin/systems/security/security-best-practices"}},
168 Standards: check.Standards{CWE: []string{"CWE-489", "CWE-209"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.3.2"}, WSTG: []string{"WSTG-CONF-02", "WSTG-ERRH-01"}, NIST: []string{"CM-6", "SI-11"}},
169 Run: func(ctx context.Context, s sys.System) []check.Result {
170 mode := regexp.MustCompile(`['"]MAGE_MODE['"]\s*=>\s*['"]([a-z]+)['"]`)
171 seen := map[string]bool{}
172 return perRoot(ctx, s, func(root string) (check.Result, bool) {
173 for _, p := range []string{root + "/app/etc/env.php", strings.TrimSuffix(root, "/pub") + "/app/etc/env.php"} {
174 if seen[p] {
175 continue
176 }
177 t, ok := noFollowText(ctx, s, p)
178 if !ok {
179 continue
180 }
181 seen[p] = true
182 m := mode.FindStringSubmatch(phpComment.ReplaceAllString(t, "$1"))
183 switch {
184 case m == nil:
185 return check.FailR(p, "MAGE_MODE is not set (default mode shows errors)"), true
186 case m[1] != "production":
187 return check.FailR(p, "MAGE_MODE is "+m[1]), true
188 }
189 return check.PassR(p, "MAGE_MODE is production"), true
190 }
191 return check.Result{}, false
192 }, "No Magento installation found in the local web roots")
193 },
194 })
195
196 check.Register(&check.Check{
197 ID: "WEB-CMS-016", Title: "Ghost Configured With a Plain HTTP Site URL", Domain: check.DomainWeb,
198 Severity: check.Medium, Category: check.CatWebApp, Effort: check.EffortLow,
199 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N",
200 Summary: "Checks Ghost config.production.json for a url that starts with http://.",
201 Rationale: "Ghost builds every link, including the admin login and member sign-in links, from the configured url. With http:// the admin session and members' tokens travel in the clear even when HTTPS is available, and browsers see mixed content.",
202 Detection: "Finds config.production.json files in the project folders that look like Ghost configuration (database and server keys) and fails when url begins with http://.",
203 Fix: "ghost config url https://<your-domain> (or edit config.production.json), set up TLS with ghost setup ssl or your reverse proxy, then ghost restart.",
204 Verify: "ghost config get url prints an https:// address.",
205 Refs: []check.Ref{refGhostConfig, {Title: "OWASP Transport Layer Security Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html"}},
206 Standards: check.Standards{CWE: []string{"CWE-319"}, OWASP: []string{"A02:2021", "A05:2021"}, ASVS: []string{"9.1.1"}, WSTG: []string{"WSTG-CRYP-03"}, NIST: []string{"SC-8", "CM-6"}},
207 Run: func(ctx context.Context, s sys.System) []check.Result {
208 urlRe := regexp.MustCompile(`"url"\s*:\s*"(https?)://`)
209 paths := collect.ProjectFiles(ctx, s, "config.production.json")
210 var out []check.Result
211 for _, p := range paths {
212 t, ok := noFollowText(ctx, s, p)
213 if !ok || !strings.Contains(t, `"database"`) || !strings.Contains(t, `"server"`) {
214 continue
215 }
216 m := urlRe.FindStringSubmatch(t)
217 switch {
218 case m == nil:
219 out = append(out, check.IncompleteR(p, "No url setting found"))
220 case m[1] == "http":
221 out = append(out, check.FailR(p, "Ghost url uses plain http://"))
222 default:
223 out = append(out, check.PassR(p, "Ghost url uses https://"))
224 }
225 }
226 if len(out) == 0 {
227 return []check.Result{check.SkipR("projects", "No Ghost production configuration found")}
228 }
229 return out
230 },
231 })
232
233 check.Register(&check.Check{
234 ID: "WEB-CMS-017", Title: "WordPress Cron Reachable by Visitors", Domain: check.DomainWeb,
235 Severity: check.Info, Category: check.CatConfig, Effort: check.EffortLow,
236 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N",
237 Summary: "Reports whether /wp-cron.php answers to anonymous requests on WordPress sites. Informational.",
238 Rationale: "WP-Cron runs scheduled tasks when visitors load pages, and wp-cron.php can be requested directly. On busy or targeted sites that lets anyone make the server do work on demand, and it is a frequent target for resource exhaustion; a system cron job is the documented production setup.",
239 Detection: "Sends one GET for /wp-cron.php on each local site and reports a 200 response with an empty or non-HTML body (what wp-cron.php returns). Sites whose wp-config.php sets DISABLE_WP_CRON to true and block the file pass.",
240 Fix: "Add define('DISABLE_WP_CRON', true); to wp-config.php, run */5 * * * * www-data php /var/www/<site>/wp-cron.php from the system cron (or curl the URL from cron), and deny direct requests to wp-cron.php at the web server.",
241 Verify: "curl -s -o /dev/null -w '%{http_code}' https://<site>/wp-cron.php prints 403 or 404.",
242 Refs: []check.Ref{refWPCron, refWPHardening},
243 Standards: check.Standards{CWE: []string{"CWE-770"}, OWASP: []string{"A05:2021"}, WSTG: []string{"WSTG-CONF-05"}, NIST: []string{"CM-7", "SC-5"}},
244 Run: func(ctx context.Context, s sys.System) []check.Result {
245 return ForEachSite(ctx, s, func(st Site) check.Result {
246 r, err := st.Get(ctx, s, "/wp-cron.php")
247 if err == nil && r.Status == 200 && (strings.TrimSpace(r.Body) == "" || !HTML(r)) {
248 return check.FailR(st.Name, "wp-cron.php answers anonymous requests")
249 }
250 return check.PassR(st.Name, "wp-cron.php is not reachable")
251 })
252 },
253 })
254
255 check.Register(&check.Check{
256 ID: "WEB-CMS-018", Title: "WordPress Author Archive Reveals Login Names", Domain: check.DomainWeb,
257 Severity: check.Low, Category: check.CatExposure, Effort: check.EffortLow,
258 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
259 Summary: "Checks whether /?author=1 redirects to an author archive, which exposes the first account's login name in the address.",
260 Rationale: "The author archive address contains the user's slug, which by default equals the login name. Together with the REST API listing (WEB-CMS-003) this gives attackers the administrator's user name for password guessing without touching the login form.",
261 Detection: "Sends one GET for /?author=1 and fails on a 301 or 302 whose Location header contains /author/. The name itself is not written to the report.",
262 Fix: "Return 404 for author queries by id (a small redirect rule or a security plugin's user enumeration protection), and give administrators a nickname and display name that differ from the login name.",
263 Verify: "curl -sI https://<site>/?author=1 | grep -i location prints nothing or a 404 page.",
264 Refs: []check.Ref{refWPBrute, refWPHardening},
265 Standards: check.Standards{CWE: []string{"CWE-204"}, OWASP: []string{"A07:2021"}, WSTG: []string{"WSTG-IDNT-04"}, NIST: []string{"CM-6", "SI-11"}},
266 Run: func(ctx context.Context, s sys.System) []check.Result {
267 return ForEachSite(ctx, s, func(st Site) check.Result {
268 r, err := st.Get(ctx, s, "/?author=1")
269 if err == nil && (r.Status == 301 || r.Status == 302) && strings.Contains(r.Header.Get("Location"), "/author/") {
270 return check.FailR(st.Name, "/?author=1 redirects to the author archive", "Location: .../author/<login-name>/")
271 }
272 return check.PassR(st.Name, "/?author=1 does not redirect to an archive")
273 })
274 },
275 })
276
277 check.Register(&check.Check{
278 ID: "WEB-CMS-019", Title: "Drupal update.php Open Without Authentication", Domain: check.DomainWeb,
279 Severity: check.High, Category: check.CatConfig, Effort: check.EffortLow,
280 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H",
281 Summary: "Checks Drupal settings.php for $settings['update_free_access'] = TRUE.",
282 Rationale: "update_free_access lets anyone run update.php, which executes database update hooks as the site. It exists for one-off maintenance and Drupal's own status report warns when it is left on.",
283 Detection: "Reads each sites/*/settings.php found through the web roots (comments removed) and fails when update_free_access is TRUE.",
284 Fix: "Set $settings['update_free_access'] = FALSE; in settings.php (the default) and run updates as user 1 or with drush updatedb.",
285 Verify: "grep update_free_access sites/default/settings.php shows FALSE.",
286 Refs: []check.Ref{refDrupalUpdate, {Title: "Drupal.org, Securing file permissions and ownership", URL: "https://www.drupal.org/docs/administering-a-drupal-site/security-in-drupal/securing-file-permissions-and-ownership"}},
287 Standards: check.Standards{CWE: []string{"CWE-306"}, OWASP: []string{"A01:2021", "A05:2021"}, ASVS: []string{"4.3.2"}, WSTG: []string{"WSTG-CONF-05"}, NIST: []string{"AC-3", "CM-6"}},
288 Run: func(ctx context.Context, s sys.System) []check.Result {
289 free := regexp.MustCompile(`(?i)\$settings\[\s*['"]update_free_access['"]\s*\]\s*=\s*TRUE`)
290 return cmsCheck(ctx, s, "Drupal", func(c collect.CMSConfig) []string {
291 if free.MatchString(phpComment.ReplaceAllString(c.Text, "$1")) {
292 return []string{"$settings['update_free_access'] is TRUE"}
293 }
294 return nil
295 }, "Drupal update.php runs without authentication")
296 },
297 })
298}

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-CMS-013 Drupal Trusted Host Patterns Not Set (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/WEB-CMS-013.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_cms_013,
  author       = {{OpenVAPT}},
  title        = {{WEB-CMS-013 Drupal Trusted Host Patterns Not Set}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/WEB-CMS-013.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.