Joomla Core Past End of Life
Reads the installed Joomla version from administrator/manifests/files/joomla.xml on disk and compares it with the Joomla support dates.
- Why It Matters
- 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.
- How It Detects
- 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.
- How to 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.
- How to Verify
grep '<version>' administrator/manifests/files/joomla.xml- CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H- Control Category
- Patch and Vulnerability Management: CIS Controls v8 7, 2; NIST CSF 2.0 ID.RA, PR.PS
- Fix Effort
- High
- CIS Controls v8 Safeguard
- 2.2
- NIST CSF 2.0 Subcategory
- ID.RA-01, PR.PS-02
- CWE
- CWE-1104
- OWASP Top 10
- A06:2021
- OWASP ASVS 4.0.3
- 14.2.1
- NIST SP 800-53 Rev. 5
- SI-2, SA-22
- References
- Joomla Documentation, Joomla Release and Support Cycle
Joomla Documentation, Security Checklist
Source Code
The complete file that defines this check. Run it with openvapt scan --only WEB-CMS-014, or read it offline with openvapt explain WEB-CMS-014.
internal/checks/web/cms_depth.go · Lines 1-298View on GitHub at This Commit
| 1 | package web |
| 2 | |
| 3 | import ( |
| 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 | |
| 19 | var ( |
| 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. |
| 32 | var 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. |
| 35 | var 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. |
| 38 | func 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. |
| 48 | func 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 | |
| 61 | func 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.
OpenVAPT. (2026). WEB-CMS-014 Joomla Core Past End of Life (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/WEB-CMS-014.html
In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
@misc{openvapt_web_cms_014,
author = {{OpenVAPT}},
title = {{WEB-CMS-014 Joomla Core Past End of Life}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/WEB-CMS-014.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.