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-HDR-007Information ExposureWeb Apps

Server Software Version Disclosed

Checks whether response headers reveal exact web server, PHP or framework versions.

Why It Matters
Exact versions let attackers match the site against public exploit lists in seconds. Hiding them does not fix vulnerabilities but removes free reconnaissance.
How It Detects
Looks for a version number in the Server header and for the X-Powered-By, X-AspNet-Version and X-Generator headers.
How to Fix
Apache: ServerTokens Prod and ServerSignature Off . nginx: server_tokens off; . PHP: expose_php = Off in php.ini . Remove X-Powered-By in the application or proxy.
How to Verify
curl -sI https://<site>/ | grep -Ei '^(server|x-powered-by)'
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-02
CWE
CWE-497
OWASP Top 10
A05:2021
OWASP ASVS 4.0.3
14.3.3
NIST SP 800-53 Rev. 5
CM-7, SI-11
References
OWASP HTTP Headers Cheat Sheet
Apache HTTP Server, ServerTokens directive
Learn the theory behind this check: P6 Information Exposure and Misconfiguration, A8 Tracing Findings to Security Requirements.

Source Code

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

internal/checks/web/headers.go · Lines 1-303View on GitHub at This Commit
1package web
2
3import (
4 "context"
5 "net/http"
6 "regexp"
7 "strconv"
8 "strings"
9
10 "github.com/openvapt/openvapt/internal/check"
11 "github.com/openvapt/openvapt/internal/sys"
12)
13
14func init() {
15 check.Register(&check.Check{
16 ID: "WEB-HDR-001",
17 Title: "Missing HTTP Strict Transport Security (HSTS)",
18 Domain: check.DomainWeb,
19 Severity: check.Medium,
20 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:L/A:N",
21 Summary: "Checks that HTTPS sites tell browsers to always use HTTPS, for at least six months.",
22 Rationale: "Without HSTS, the first visit or any typed http:// link can be intercepted and downgraded to plain HTTP (SSL stripping) on public Wi-Fi.",
23 Detection: "Reads the Strict-Transport-Security header from each HTTPS site's home page, including a redirect response, because browsers honour HSTS on any HTTPS response. Plain HTTP responses are not evaluated. Missing, or max-age below 15552000 seconds (180 days), fails.",
24 Fix: "Apache: Header always set Strict-Transport-Security \"max-age=31536000; includeSubDomains\" . nginx: add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains\" always; . Only add 'preload' once every subdomain supports HTTPS.",
25 Verify: "curl -sI https://<site>/ | grep -i strict-transport-security",
26 Refs: []check.Ref{{Title: "RFC 6797, HTTP Strict Transport Security", URL: "https://www.rfc-editor.org/rfc/rfc6797.html"}, refHTTPHeadersCS, refMDN("Strict-Transport-Security")},
27 Standards: check.Standards{CWE: []string{"CWE-319", "CWE-523"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.4.5"}, WSTG: []string{"WSTG-CONF-07"}, NIST: []string{"SC-8"}},
28 Run: func(ctx context.Context, s sys.System) []check.Result {
29 return ForEachSite(ctx, s, func(st Site) check.Result {
30 // Browsers honour HSTS on any HTTPS response, redirects
31 // included (RFC 6797 section 8.1), so a 3xx is evaluated too.
32 if !st.HTTPS {
33 return check.SkipR(st.Name, "Site not served over HTTPS (see WEB-TLS-001)")
34 }
35 h := st.Home.Header.Get("Strict-Transport-Security")
36 if h == "" {
37 return check.FailR(st.Name, "Strict-Transport-Security header is missing")
38 }
39 n, ok := hstsMaxAge(h)
40 if !ok {
41 return check.FailR(st.Name, "HSTS max-age is missing, duplicated or invalid", h)
42 }
43 if n < 15552000 {
44 return check.FailR(st.Name, "HSTS max-age is shorter than 180 days", h)
45 }
46 return check.PassR(st.Name, "HSTS is set", h)
47 })
48 },
49 })
50
51 check.Register(&check.Check{
52 ID: "WEB-HDR-002",
53 Title: "Missing Content Security Policy",
54 Domain: check.DomainWeb,
55 Severity: check.Medium,
56 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
57 Summary: "Checks that each site sends a Content-Security-Policy header.",
58 Rationale: "A CSP limits where scripts may load from, turning many cross-site scripting (XSS) bugs from account takeover into a blocked request.",
59 Detection: "Reads Content-Security-Policy from each site's home page. Missing fails. A policy containing 'unsafe-inline' in script-src (without a nonce or hash) or a bare * source is reported as weak.",
60 Fix: "Start with a report-only policy to find what the site loads, then enforce, for example: Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'",
61 Verify: "curl -sI https://<site>/ | grep -i content-security-policy",
62 Refs: []check.Ref{{Title: "OWASP Content Security Policy Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html"}, {Title: "W3C Content Security Policy Level 3", URL: "https://www.w3.org/TR/CSP3/"}},
63 Standards: check.Standards{CWE: []string{"CWE-1021", "CWE-79"}, OWASP: []string{"A03:2021", "A05:2021"}, ASVS: []string{"14.4.3"}, WSTG: []string{"WSTG-CONF-12"}, NIST: []string{"SI-10"}},
64 Run: func(ctx context.Context, s sys.System) []check.Result {
65 return ForEachSite(ctx, s, func(st Site) check.Result {
66 if r, skip := st.Redirected(); skip {
67 return r
68 }
69 h := st.Home.Header.Get("Content-Security-Policy")
70 if h == "" {
71 return check.FailR(st.Name, "Content-Security-Policy header is missing")
72 }
73 if reason := weakCSP(h); reason != "" {
74 return check.FailR(st.Name, reason, h)
75 }
76 return check.PassR(st.Name, "Content-Security-Policy is set", h)
77 })
78 },
79 })
80
81 check.Register(&check.Check{
82 ID: "WEB-HDR-003",
83 Title: "Site Can Be Framed (Clickjacking)",
84 Domain: check.DomainWeb,
85 Severity: check.Medium,
86 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N",
87 Summary: "Checks that each site forbids being embedded in other sites' frames.",
88 Rationale: "If a page can be framed, an attacker can overlay it invisibly and trick users into clicking buttons (clickjacking), such as 'delete' or 'transfer'.",
89 Detection: "Passes if the CSP frame-ancestors directive restricts framing (a * or scheme-only source fails), or, without frame-ancestors, X-Frame-Options is DENY or SAMEORIGIN (repeated identical values are accepted; conflicting values fail).",
90 Fix: "Send Content-Security-Policy: frame-ancestors 'self' (modern) and X-Frame-Options: SAMEORIGIN (older browsers).",
91 Verify: "curl -sI https://<site>/ | grep -Ei 'x-frame-options|frame-ancestors'",
92 Refs: []check.Ref{{Title: "OWASP Clickjacking Defense Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html"}, refMDN("X-Frame-Options")},
93 Standards: check.Standards{CWE: []string{"CWE-1021"}, OWASP: []string{"A04:2021"}, ASVS: []string{"14.4.7"}, WSTG: []string{"WSTG-CLNT-09"}, NIST: []string{"SC-18"}},
94 Run: func(ctx context.Context, s sys.System) []check.Result {
95 return ForEachSite(ctx, s, func(st Site) check.Result {
96 if r, skip := st.Redirected(); skip {
97 return r
98 }
99 ok, why := framingRestricted(st.Home.Header)
100 if ok {
101 return check.PassR(st.Name, "Framing is restricted", why)
102 }
103 return check.FailR(st.Name, why)
104 })
105 },
106 })
107
108 simpleHeader := func(id, title, header, want, summary, rationale, fix string, sev check.Severity, std check.Standards) {
109 check.Register(&check.Check{
110 ID: id, Title: title, Domain: check.DomainWeb, Severity: sev,
111 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N",
112 Summary: summary,
113 Rationale: rationale,
114 Detection: "Reads the " + header + " header from each site's home page and checks that it is present" + map[bool]string{true: " with the value " + want, false: ""}[want != ""] + ".",
115 Fix: fix,
116 Verify: "curl -sI https://<site>/ | grep -i " + strings.ToLower(header),
117 Refs: []check.Ref{refSecureHeaders, refMDN(header)},
118 Standards: std,
119 Run: func(ctx context.Context, s sys.System) []check.Result {
120 return ForEachSite(ctx, s, func(st Site) check.Result {
121 if r, skip := st.Redirected(); skip {
122 return r
123 }
124 v := st.Home.Header.Get(header)
125 if v == "" || (want != "" && !strings.EqualFold(strings.TrimSpace(v), want)) {
126 return check.FailR(st.Name, header+" is missing or not '"+want+"'", v)
127 }
128 if header == "Referrer-Policy" && unsafeReferrer(st.Home.Header.Values(header)) {
129 return check.FailR(st.Name, "Referrer-Policy unsafe-url sends the full URL to every site", v)
130 }
131 return check.PassR(st.Name, header+" is set", v)
132 })
133 },
134 })
135 }
136 simpleHeader("WEB-HDR-004", "Missing X-Content-Type-Options", "X-Content-Type-Options", "nosniff",
137 "Checks that browsers are told not to guess (sniff) content types.",
138 "Without nosniff, a browser may execute an uploaded text or image file as script or HTML, enabling XSS through file uploads.",
139 "Send X-Content-Type-Options: nosniff on every response (Apache: Header always set X-Content-Type-Options nosniff; nginx: add_header X-Content-Type-Options nosniff always;).",
140 check.Low, check.Standards{CWE: []string{"CWE-430"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.4.4"}, NIST: []string{"CM-6"}})
141 simpleHeader("WEB-HDR-005", "Missing Referrer-Policy", "Referrer-Policy", "",
142 "Checks that each site limits what URL information is leaked to other sites in the Referer header.",
143 "Full URLs can contain session tokens, password-reset links or personal data, which would otherwise be sent to every third-party resource and outbound link.",
144 "Send Referrer-Policy: strict-origin-when-cross-origin (or no-referrer). Never use unsafe-url.",
145 check.Low, check.Standards{CWE: []string{"CWE-201"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.4.6"}, NIST: []string{"CM-6"}})
146 simpleHeader("WEB-HDR-006", "Missing Permissions-Policy", "Permissions-Policy", "",
147 "Checks that each site restricts powerful browser features (camera, microphone, geolocation) it does not use.",
148 "Restricting features limits what injected or third-party scripts can do in your visitors' browsers.",
149 "Send, for example, Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=() adjusted to the features the site really uses.",
150 check.Low, check.Standards{CWE: []string{"CWE-1188"}, OWASP: []string{"A05:2021"}, NIST: []string{"CM-6", "CM-7"}})
151
152 check.Register(&check.Check{
153 ID: "WEB-HDR-007",
154 Title: "Server Software Version Disclosed",
155 Domain: check.DomainWeb,
156 Severity: check.Low,
157 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
158 Summary: "Checks whether response headers reveal exact web server, PHP or framework versions.",
159 Rationale: "Exact versions let attackers match the site against public exploit lists in seconds. Hiding them does not fix vulnerabilities but removes free reconnaissance.",
160 Detection: "Looks for a version number in the Server header and for the X-Powered-By, X-AspNet-Version and X-Generator headers.",
161 Fix: "Apache: ServerTokens Prod and ServerSignature Off . nginx: server_tokens off; . PHP: expose_php = Off in php.ini . Remove X-Powered-By in the application or proxy.",
162 Verify: "curl -sI https://<site>/ | grep -Ei '^(server|x-powered-by)'",
163 Refs: []check.Ref{refHTTPHeadersCS, {Title: "Apache HTTP Server, ServerTokens directive", URL: "https://httpd.apache.org/docs/2.4/mod/core.html#servertokens"}},
164 Standards: check.Standards{CWE: []string{"CWE-497"}, OWASP: []string{"A05:2021"}, ASVS: []string{"14.3.3"}, WSTG: []string{"WSTG-INFO-02"}, NIST: []string{"CM-7", "SI-11"}},
165 Run: func(ctx context.Context, s sys.System) []check.Result {
166 ver := regexp.MustCompile(`\d+\.\d+`)
167 return ForEachSite(ctx, s, func(st Site) check.Result {
168 if r, skip := st.Redirected(); skip {
169 return r
170 }
171 var ev []string
172 if v := st.Home.Header.Get("Server"); ver.MatchString(v) {
173 ev = append(ev, "Server: "+v)
174 }
175 for _, h := range []string{"X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version", "X-Generator"} {
176 if v := st.Home.Header.Get(h); v != "" {
177 ev = append(ev, h+": "+v)
178 }
179 }
180 if len(ev) > 0 {
181 return check.FailR(st.Name, "Software versions are disclosed in headers", ev...)
182 }
183 return check.PassR(st.Name, "No version details in headers")
184 })
185 },
186 })
187}
188
189func hstsMaxAge(h string) (uint64, bool) {
190 var value string
191 for _, part := range strings.Split(h, ";") {
192 kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
193 if len(kv) == 2 && strings.EqualFold(strings.TrimSpace(kv[0]), "max-age") {
194 if value != "" {
195 return 0, false
196 }
197 value = strings.TrimSpace(kv[1])
198 }
199 }
200 // RFC 6797 allows max-age as a token or a quoted string.
201 if len(value) >= 2 && strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
202 value = value[1 : len(value)-1]
203 }
204 if value == "" {
205 return 0, false
206 }
207 n, err := strconv.ParseUint(value, 10, 64)
208 return n, err == nil
209}
210
211func weakCSP(h string) string {
212 dirs := map[string][]string{}
213 for _, raw := range strings.Split(strings.ToLower(h), ";") {
214 f := strings.Fields(raw)
215 if len(f) > 0 {
216 dirs[f[0]] = f[1:]
217 }
218 }
219 script := dirs["script-src"]
220 if len(script) == 0 {
221 script = dirs["default-src"]
222 }
223 for _, src := range script {
224 if src == "*" || src == "data:" || strings.HasPrefix(src, "http:") {
225 return "CSP permits an unsafe script source"
226 }
227 if src == "'unsafe-eval'" {
228 return "CSP allows unsafe script evaluation"
229 }
230 if src == "'unsafe-inline'" && !hasNonceOrHash(script) {
231 return "CSP allows inline scripts without a nonce or hash"
232 }
233 }
234 if _, ok := dirs["object-src"]; !ok {
235 return "CSP does not restrict object-src"
236 }
237 return ""
238}
239func hasNonceOrHash(src []string) bool {
240 for _, s := range src {
241 if strings.HasPrefix(s, "'nonce-") || strings.HasPrefix(s, "'sha256-") || strings.HasPrefix(s, "'sha384-") || strings.HasPrefix(s, "'sha512-") {
242 return true
243 }
244 }
245 return false
246}
247
248// framingRestricted evaluates CSP frame-ancestors first (browsers ignore
249// X-Frame-Options when it is present), then X-Frame-Options.
250func framingRestricted(h http.Header) (bool, string) {
251 for _, csp := range h.Values("Content-Security-Policy") {
252 for _, raw := range strings.Split(strings.ToLower(csp), ";") {
253 f := strings.Fields(raw)
254 if len(f) == 0 || f[0] != "frame-ancestors" {
255 continue
256 }
257 for _, src := range f[1:] {
258 if src == "*" || src == "http:" || src == "https:" || src == "*:" {
259 return false, "CSP frame-ancestors allows any site to frame the page (" + strings.Join(f[1:], " ") + ")"
260 }
261 }
262 if len(f) == 1 {
263 return false, "CSP frame-ancestors has no source list"
264 }
265 return true, "frame-ancestors " + strings.Join(f[1:], " ")
266 }
267 }
268 var vals []string
269 for _, v := range h.Values("X-Frame-Options") {
270 for _, p := range strings.Split(v, ",") {
271 if p = strings.ToUpper(strings.TrimSpace(p)); p != "" {
272 vals = append(vals, p)
273 }
274 }
275 }
276 if len(vals) == 0 {
277 return false, "Neither X-Frame-Options nor CSP frame-ancestors is set"
278 }
279 for _, v := range vals[1:] {
280 if v != vals[0] {
281 return false, "X-Frame-Options has conflicting values (" + strings.Join(vals, ", ") + "), which browsers ignore"
282 }
283 }
284 if vals[0] == "DENY" || vals[0] == "SAMEORIGIN" {
285 return true, "X-Frame-Options " + vals[0]
286 }
287 return false, "X-Frame-Options value " + vals[0] + " is not DENY or SAMEORIGIN"
288}
289
290// unsafeReferrer reports whether the effective Referrer-Policy (the last
291// recognised token, per the W3C Referrer Policy specification) is unsafe-url.
292func unsafeReferrer(vals []string) bool {
293 last := ""
294 for _, v := range vals {
295 for _, t := range strings.Split(v, ",") {
296 switch t = strings.ToLower(strings.TrimSpace(t)); t {
297 case "no-referrer", "no-referrer-when-downgrade", "same-origin", "origin", "strict-origin", "origin-when-cross-origin", "strict-origin-when-cross-origin", "unsafe-url":
298 last = t
299 }
300 }
301 }
302 return last == "unsafe-url"
303}

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