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
LowWEB-CMS-010Information ExposureWeb Apps

Joomla Version Disclosed

Checks whether a Joomla site serves its version manifest at /administrator/manifests/files/joomla.xml.

Why It Matters
The manifest gives the exact Joomla version, which lets automated tools pick exploits for known vulnerabilities, such as the unauthenticated information disclosure CVE-2023-23752.
How It Detects
Sends one GET for /administrator/manifests/files/joomla.xml and fails on a 200 response that contains a <version> element.
How to Fix
Block the folder at the web server (Apache: <Directory "/path/administrator/manifests"> Require all denied </Directory>; nginx: location ^~ /administrator/manifests/ { deny all; }) and keep Joomla updated.
How to Verify
curl -s -o /dev/null -w '%{http_code}' https://<site>/administrator/manifests/files/joomla.xml prints 403 or 404.
CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/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-INFO-08
CWE
CWE-497
OWASP Top 10
A05:2021, A06:2021
OWASP ASVS 4.0.3
14.3.3
NIST SP 800-53 Rev. 5
SI-11
References
Joomla Documentation, Security Checklist
NVD, CVE-2023-23752
Learn the theory behind this check: P6 Information Exposure and Misconfiguration, P14 CMS Hardening in Depth: WordPress, Joomla and Drupal.

Source Code

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

internal/checks/web/cms_more.go · Lines 1-255View on GitHub at This Commit
1package web
2
3import (
4 "context"
5 "fmt"
6 "regexp"
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 WordPress, Joomla and Drupal checks. Configuration files are found
15// through the local web roots and read for a few settings only; they hold
16// database passwords, so nothing but the path and the setting name is ever
17// reported. HTTP checks send one GET for a fixed public file.
18
19var refWPHardening = check.Ref{Title: "WordPress Developer Resources, Hardening WordPress", URL: "https://developer.wordpress.org/advanced-administration/security/hardening/"}
20var refWPConfig = check.Ref{Title: "WordPress Developer Resources, Editing wp-config.php", URL: "https://developer.wordpress.org/advanced-administration/wordpress/wp-config/"}
21
22// phpComment removes PHP comments (// ..., # ..., /* ... */) so a
23// commented-out define() is never read as active configuration.
24var phpComment = regexp.MustCompile(`(?s:/\*.*?\*/)|(?m:(^|[^:])//[^\n]*)|(?m:^[ \t]*#[^\n]*)`)
25
26// wpDefine returns the value of the first active define('NAME', value):
27// PHP keeps the first definition of a constant and ignores later ones.
28func wpDefine(text, name string) (string, bool) {
29 text = phpComment.ReplaceAllString(text, "$1")
30 re := regexp.MustCompile(`(?i)define\s*\(\s*['"]` + regexp.QuoteMeta(name) + `['"]\s*,\s*(.+?)\s*\)\s*;`)
31 m := re.FindStringSubmatch(text)
32 if m == nil {
33 return "", false
34 }
35 return strings.ToLower(strings.Trim(m[1], `'" `)), true
36}
37
38func cmsCheck(ctx context.Context, s sys.System, cms string, judge func(collect.CMSConfig) []string, failMsg string) []check.Result {
39 var out []check.Result
40 for _, c := range collect.CMSConfigs(ctx, s) {
41 if cms != "" && c.CMS != cms {
42 continue
43 }
44 if bad := judge(c); len(bad) > 0 {
45 out = append(out, check.FailR(c.Path, failMsg, bad...))
46 } else {
47 out = append(out, check.PassR(c.Path, "No issue found"))
48 }
49 }
50 if len(out) == 0 {
51 name := cms
52 if name == "" {
53 name = "WordPress, Joomla or Drupal"
54 }
55 return []check.Result{check.SkipR("web roots", "No "+name+" configuration found in the local web roots")}
56 }
57 return out
58}
59
60func modeCheck(s sys.System, c collect.CMSConfig, mask uint32, what string) []string {
61 st, err := s.Stat(c.Path)
62 if err != nil {
63 return nil
64 }
65 if uint32(st.Mode.Perm())&mask != 0 {
66 return []string{fmt.Sprintf("%s configuration has mode %04o (%s)", c.CMS, st.Mode.Perm(), what)}
67 }
68 return nil
69}
70
71func init() {
72 check.Register(&check.Check{
73 ID: "WEB-CMS-004", Title: "CMS Configuration Readable by All Users", Domain: check.DomainWeb,
74 Severity: check.High, Category: check.CatAccess, Effort: check.EffortLow,
75 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
76 Summary: "Checks whether wp-config.php, Joomla configuration.php or Drupal settings.php can be read by every local user.",
77 Rationale: "These files hold the database password and the keys that sign logins. If every account on the machine can read them, any other site, user or compromised process on the server can take over the CMS and its data.",
78 Detection: "Finds the configuration files through the local web roots (WordPress also one folder above the root) and reads their permission bits. Fails when the world-read bit is set.",
79 Fix: "chmod 640 <file> and chown it to the site owner with the web server's group (for example chown deploy:www-data wp-config.php); on PHP-FPM pools that run as the site user, 600 or 400 also works. Rotate the database password if other people have shell access.",
80 Verify: "stat -c '%a %U:%G %n' <file> shows 640, 600 or 400.",
81 Refs: []check.Ref{refWPHardening, {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"}},
82 Standards: check.Standards{CWE: []string{"CWE-732", "CWE-260"}, OWASP: []string{"A05:2021"}, WSTG: []string{"WSTG-CONF-09"}, NIST: []string{"AC-3", "SC-28"}},
83 Run: func(ctx context.Context, s sys.System) []check.Result {
84 return cmsCheck(ctx, s, "", func(c collect.CMSConfig) []string { return modeCheck(s, c, 0o004, "readable by all users") }, "CMS configuration is readable by all users")
85 },
86 })
87
88 check.Register(&check.Check{
89 ID: "WEB-CMS-005", Title: "CMS Configuration Writable by Other Users", Domain: check.DomainWeb,
90 Severity: check.High, Category: check.CatAccess, Effort: check.EffortLow,
91 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
92 Summary: "Checks whether wp-config.php, Joomla configuration.php or Drupal settings.php is group- or world-writable.",
93 Rationale: "These files are PHP code that runs on every request. Whoever can write them can add a backdoor that runs as the web server, which is how many shared-hosting compromises spread from one site to the next.",
94 Detection: "Finds the configuration files through the local web roots and fails when the group-write or world-write permission bit is set.",
95 Fix: "chmod 640 (or 440) <file>. Drupal also expects sites/default to be 555 after installation. Review the file for code you did not add.",
96 Verify: "stat -c '%a %n' <file> shows no write bit for group or others.",
97 Refs: []check.Ref{refWPHardening, {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"}},
98 Standards: check.Standards{CWE: []string{"CWE-732"}, OWASP: []string{"A05:2021"}, WSTG: []string{"WSTG-CONF-09"}, NIST: []string{"AC-3", "CM-5"}},
99 Run: func(ctx context.Context, s sys.System) []check.Result {
100 return cmsCheck(ctx, s, "", func(c collect.CMSConfig) []string { return modeCheck(s, c, 0o022, "writable by group or others") }, "CMS configuration is writable by other users")
101 },
102 })
103
104 check.Register(&check.Check{
105 ID: "WEB-CMS-006", Title: "WordPress Shows Debug Output to Visitors", Domain: check.DomainWeb,
106 Severity: check.Medium, Category: check.CatExposure, Effort: check.EffortLow,
107 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
108 Summary: "Checks wp-config.php for WP_DEBUG enabled without WP_DEBUG_DISPLAY set to false.",
109 Rationale: "With WP_DEBUG on and display not turned off, PHP notices and errors are printed into pages, revealing server paths, plugin names and sometimes database details to anyone browsing the site.",
110 Detection: "Reads define('WP_DEBUG', ...) and define('WP_DEBUG_DISPLAY', ...) from each wp-config.php found through the web roots.",
111 Fix: "In production set define('WP_DEBUG', false);. If you need logs, use define('WP_DEBUG_LOG', '/path/outside/webroot/wp-debug.log'); together with define('WP_DEBUG_DISPLAY', false);.",
112 Verify: "grep -E \"WP_DEBUG(_DISPLAY)?'\" wp-config.php",
113 Refs: []check.Ref{{Title: "WordPress Developer Resources, Debugging in WordPress", URL: "https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/"}},
114 Standards: check.Standards{CWE: []string{"CWE-209", "CWE-489"}, OWASP: []string{"A05:2021"}, WSTG: []string{"WSTG-ERRH-01"}, NIST: []string{"SI-11"}},
115 Run: func(ctx context.Context, s sys.System) []check.Result {
116 return cmsCheck(ctx, s, "WordPress", func(c collect.CMSConfig) []string {
117 dbg, _ := wpDefine(c.Text, "WP_DEBUG")
118 disp, set := wpDefine(c.Text, "WP_DEBUG_DISPLAY")
119 if dbg == "true" && (!set || disp == "true" || disp == "1") {
120 return []string{"WP_DEBUG is true and WP_DEBUG_DISPLAY is not false"}
121 }
122 return nil
123 }, "WordPress displays debug output")
124 },
125 })
126
127 check.Register(&check.Check{
128 ID: "WEB-CMS-007", Title: "WordPress Dashboard File Editor Enabled", Domain: check.DomainWeb,
129 Severity: check.Low, Category: check.CatConfig, Effort: check.EffortLow,
130 CVSS: "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N",
131 Summary: "Checks whether wp-config.php leaves the built-in theme and plugin file editor enabled.",
132 Rationale: "The editor lets any administrator account change PHP files from the browser. A stolen or phished admin session then becomes code execution on the server in one step.",
133 Detection: "Reads each wp-config.php found through the web roots and fails unless DISALLOW_FILE_EDIT or DISALLOW_FILE_MODS is defined as true.",
134 Fix: "Add define('DISALLOW_FILE_EDIT', true); to wp-config.php above the line that says to stop editing.",
135 Verify: "The Theme File Editor and Plugin File Editor menus disappear from the dashboard.",
136 Refs: []check.Ref{refWPHardening, refWPConfig},
137 Standards: check.Standards{CWE: []string{"CWE-94", "CWE-1188"}, OWASP: []string{"A05:2021"}, NIST: []string{"CM-7", "AC-6"}},
138 Run: func(ctx context.Context, s sys.System) []check.Result {
139 return cmsCheck(ctx, s, "WordPress", func(c collect.CMSConfig) []string {
140 for _, k := range []string{"DISALLOW_FILE_EDIT", "DISALLOW_FILE_MODS"} {
141 if v, ok := wpDefine(c.Text, k); ok && (v == "true" || v == "1") {
142 return nil
143 }
144 }
145 return []string{"DISALLOW_FILE_EDIT is not set to true"}
146 }, "WordPress file editor is enabled")
147 },
148 })
149
150 check.Register(&check.Check{
151 ID: "WEB-CMS-008", Title: "WordPress Automatic Updates Disabled", Domain: check.DomainWeb,
152 Severity: check.Medium, Category: check.CatPatch, Effort: check.EffortLow,
153 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
154 Summary: "Checks whether wp-config.php turns off WordPress background updates, including security releases.",
155 Rationale: "WordPress ships security fixes as automatic minor updates. Turning the updater off means the site stays vulnerable until someone remembers to update by hand, which is how most compromised WordPress sites fall behind.",
156 Detection: "Reads each wp-config.php found through the web roots and fails when AUTOMATIC_UPDATER_DISABLED is true or WP_AUTO_UPDATE_CORE is false.",
157 Fix: "Remove those lines, or set define('WP_AUTO_UPDATE_CORE', 'minor'); so at least security releases install automatically. Keep backups and enable plugin auto-updates where practical.",
158 Verify: "Dashboard, Updates shows that automatic updates for maintenance and security releases are on.",
159 Refs: []check.Ref{{Title: "WordPress Developer Resources, Configuring Automatic Background Updates", URL: "https://developer.wordpress.org/advanced-administration/upgrade/upgrading/#configuring-automatic-background-updates"}},
160 Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, NIST: []string{"SI-2"}},
161 Run: func(ctx context.Context, s sys.System) []check.Result {
162 return cmsCheck(ctx, s, "WordPress", func(c collect.CMSConfig) []string {
163 var bad []string
164 if v, ok := wpDefine(c.Text, "AUTOMATIC_UPDATER_DISABLED"); ok && (v == "true" || v == "1") {
165 bad = append(bad, "AUTOMATIC_UPDATER_DISABLED is true")
166 }
167 if v, ok := wpDefine(c.Text, "WP_AUTO_UPDATE_CORE"); ok && v == "false" {
168 bad = append(bad, "WP_AUTO_UPDATE_CORE is false")
169 }
170 return bad
171 }, "WordPress automatic updates are disabled")
172 },
173 })
174
175 joomlaVar := func(text, name string) string {
176 m := regexp.MustCompile(`(?m)public\s+\$` + name + `\s*=\s*['"]?([^'";]*)`).FindStringSubmatch(text)
177 if m == nil {
178 return ""
179 }
180 return strings.ToLower(m[1])
181 }
182 check.Register(&check.Check{
183 ID: "WEB-CMS-009", Title: "Joomla Debug or Verbose Error Reporting Enabled", Domain: check.DomainWeb,
184 Severity: check.Medium, Category: check.CatExposure, Effort: check.EffortLow,
185 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
186 Summary: "Checks Joomla configuration.php for debug mode or maximum/development error reporting.",
187 Rationale: "Joomla's debug console and verbose error reporting print queries, file paths and configuration details into pages that visitors can see.",
188 Detection: "Reads configuration.php found through the web roots and fails when $debug is 1 or $error_reporting is maximum or development.",
189 Fix: "In System, Global Configuration set Debug System to No and Error Reporting to None (or System Default), or set public $debug = false; and public $error_reporting = 'none'; in configuration.php.",
190 Verify: "grep -E 'debug|error_reporting' configuration.php",
191 Refs: []check.Ref{{Title: "Joomla Documentation, Security Checklist: Site Administration", URL: "https://docs.joomla.org/Security_Checklist/Site_Administration"}},
192 Standards: check.Standards{CWE: []string{"CWE-209", "CWE-489"}, OWASP: []string{"A05:2021"}, WSTG: []string{"WSTG-ERRH-01"}, NIST: []string{"SI-11"}},
193 Run: func(ctx context.Context, s sys.System) []check.Result {
194 return cmsCheck(ctx, s, "Joomla", func(c collect.CMSConfig) []string {
195 var bad []string
196 if v := joomlaVar(c.Text, "debug"); v == "1" || v == "true" {
197 bad = append(bad, "$debug is enabled")
198 }
199 if v := joomlaVar(c.Text, "error_reporting"); v == "maximum" || v == "development" {
200 bad = append(bad, "$error_reporting is "+v)
201 }
202 return bad
203 }, "Joomla debug output is enabled")
204 },
205 })
206
207 check.Register(&check.Check{
208 ID: "WEB-CMS-010", Title: "Joomla Version Disclosed", Domain: check.DomainWeb,
209 Severity: check.Low, Category: check.CatExposure, Effort: check.EffortLow,
210 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
211 Summary: "Checks whether a Joomla site serves its version manifest at /administrator/manifests/files/joomla.xml.",
212 Rationale: "The manifest gives the exact Joomla version, which lets automated tools pick exploits for known vulnerabilities, such as the unauthenticated information disclosure CVE-2023-23752.",
213 Detection: "Sends one GET for /administrator/manifests/files/joomla.xml and fails on a 200 response that contains a <version> element.",
214 Fix: "Block the folder at the web server (Apache: <Directory \"/path/administrator/manifests\"> Require all denied </Directory>; nginx: location ^~ /administrator/manifests/ { deny all; }) and keep Joomla updated.",
215 Verify: "curl -s -o /dev/null -w '%{http_code}' https://<site>/administrator/manifests/files/joomla.xml prints 403 or 404.",
216 Refs: []check.Ref{{Title: "Joomla Documentation, Security Checklist", URL: "https://docs.joomla.org/Security_Checklist"}, {Title: "NVD, CVE-2023-23752", URL: "https://nvd.nist.gov/vuln/detail/cve-2023-23752"}},
217 Standards: check.Standards{CWE: []string{"CWE-497"}, OWASP: []string{"A05:2021", "A06:2021"}, WSTG: []string{"WSTG-INFO-08"}, NIST: []string{"SI-11"}},
218 Run: func(ctx context.Context, s sys.System) []check.Result {
219 ver := regexp.MustCompile(`<version>([^<]+)</version>`)
220 return ForEachSite(ctx, s, func(st Site) check.Result {
221 r, err := st.Get(ctx, s, "/administrator/manifests/files/joomla.xml")
222 if err == nil && r.Status == 200 {
223 if m := ver.FindStringSubmatch(r.Body); m != nil && strings.Contains(r.Body, "oomla") {
224 return check.FailR(st.Name, "Joomla version manifest is public", "version "+m[1])
225 }
226 }
227 return check.PassR(st.Name, "No Joomla version manifest is served")
228 })
229 },
230 })
231
232 check.Register(&check.Check{
233 ID: "WEB-CMS-011", Title: "Drupal Version Disclosed by Changelog", Domain: check.DomainWeb,
234 Severity: check.Low, Category: check.CatExposure, Effort: check.EffortLow,
235 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
236 Summary: "Checks whether a Drupal site serves CHANGELOG.txt, which names the exact Drupal version.",
237 Rationale: "The changelog tells attackers precisely which Drupal security advisories apply, as was used at scale for Drupalgeddon 2 (CVE-2018-7600).",
238 Detection: "Sends one GET each for /CHANGELOG.txt and /core/CHANGELOG.txt and fails on a 200 response that mentions Drupal.",
239 Fix: "Delete the changelog files after each update, or deny *.txt files in the Drupal root and core folders at the web server, and keep Drupal updated.",
240 Verify: "curl -s -o /dev/null -w '%{http_code}' https://<site>/core/CHANGELOG.txt prints 403 or 404.",
241 Refs: []check.Ref{{Title: "Drupal.org, Security advisories", URL: "https://www.drupal.org/security"}, {Title: "NVD, CVE-2018-7600", URL: "https://nvd.nist.gov/vuln/detail/cve-2018-7600"}},
242 Standards: check.Standards{CWE: []string{"CWE-497"}, OWASP: []string{"A05:2021", "A06:2021"}, WSTG: []string{"WSTG-INFO-08"}, NIST: []string{"SI-11"}},
243 Run: func(ctx context.Context, s sys.System) []check.Result {
244 return ForEachSite(ctx, s, func(st Site) check.Result {
245 for _, p := range []string{"/CHANGELOG.txt", "/core/CHANGELOG.txt"} {
246 if r, err := st.Get(ctx, s, p); err == nil && r.Status == 200 && strings.Contains(r.Body, "Drupal") && !HTML(r) {
247 line := strings.TrimSpace(strings.SplitN(strings.TrimSpace(r.Body), "\n", 2)[0])
248 return check.FailR(st.Name, "Drupal changelog is public", p+": "+line)
249 }
250 }
251 return check.PassR(st.Name, "No Drupal changelog is served")
252 })
253 },
254 })
255}

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-010 Joomla Version Disclosed (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/WEB-CMS-010.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_010,
  author       = {{OpenVAPT}},
  title        = {{WEB-CMS-010 Joomla Version Disclosed}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/WEB-CMS-010.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.