CMS Configuration Readable by All Users
Checks whether wp-config.php, Joomla configuration.php or Drupal settings.php can be read by every local user.
- Why It Matters
- 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.
- How It Detects
- 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.
- How to 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.
- How to Verify
stat -c '%a %U:%G %n' <file> shows 640, 600 or 400.- CVSS v3.1 Vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N- Control Category
- Access Control: CIS Controls v8 3, 6; NIST CSF 2.0 PR.AA, PR.DS
- Fix Effort
- Low
- CIS Controls v8 Safeguard
- 3.3
- NIST CSF 2.0 Subcategory
- PR.AA-05, PR.DS-01
- OWASP WSTG v4.2
- WSTG-CONF-09
- CWE
- CWE-732, CWE-260
- OWASP Top 10
- A05:2021
- NIST SP 800-53 Rev. 5
- AC-3, SC-28
- References
- WordPress Developer Resources, Hardening WordPress
Drupal.org, Securing file permissions and ownership
Source Code
The complete file that defines this check. Run it with openvapt scan --only WEB-CMS-004, or read it offline with openvapt explain WEB-CMS-004.
internal/checks/web/cms_more.go · Lines 1-255View on GitHub at This Commit
| 1 | package web |
| 2 | |
| 3 | import ( |
| 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 | |
| 19 | var refWPHardening = check.Ref{Title: "WordPress Developer Resources, Hardening WordPress", URL: "https://developer.wordpress.org/advanced-administration/security/hardening/"} |
| 20 | var 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. |
| 24 | var 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. |
| 28 | func 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 | |
| 38 | func 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 | |
| 60 | func 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 | |
| 71 | func 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.
OpenVAPT. (2026). WEB-CMS-004 CMS Configuration Readable by All Users (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/WEB-CMS-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.
@misc{openvapt_web_cms_004,
author = {{OpenVAPT}},
title = {{WEB-CMS-004 CMS Configuration Readable by All Users}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/WEB-CMS-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.