The Nessus finding “Web Application Potentially Vulnerable to Clickjacking” means your web server returns pages without an X-Frame-Options header or a Content-Security-Policy frame-ancestors directive, so any other website can load them inside an invisible iframe. Fix it by sending X-Frame-Options: SAMEORIGIN and Content-Security-Policy: frame-ancestors 'self' on every HTML response, then rescan.
What the scanner is actually detecting
Nessus plugin 85582, titled Web Application Potentially Vulnerable to Clickjacking, belongs to the Web Servers family and is rated Medium by Tenable. It is a non-intrusive remote check: the scanner requests pages and inspects the response headers. Nothing is exploited. According to Tenable’s description, it fires when the server does not set an X-Frame-Options header or a Content-Security-Policy frame-ancestors header in all content responses.
Tenable Web App Scanning reports the equivalent condition as plugin 98060, Missing ‘X-Frame-Options’ Header, rated Low. Its title and solution refer only to X-Frame-Options, so a fix that sends CSP alone may not clear it. Qualys WAS and Greenbone/OpenVAS report missing framing headers in their own security-header checks (in Greenbone often as an informational log result). In each case, the URLs listed in the finding detail are your test cases.
How serious is clickjacking in practice?
Clickjacking (also called UI redress) works by loading your page in a transparent iframe on an attacker’s site and positioning it so the victim clicks a real button on your page while believing they are clicking something else. For the attack to matter, three things must line up: the framed page performs a meaningful action in one or two clicks, the victim is already signed in so the browser sends the session cookie into the frame, and the victim visits the attacker’s page.
- Higher exposure: admin consoles, account and security settings, pages with one-click approve, delete or transfer buttons, and network appliance management UIs.
- Lower exposure: static marketing pages, public documentation, and JSON APIs that a browser never renders as a document.
Session cookies marked SameSite=Lax or Strict are not sent into a cross-site iframe, which blunts attacks on authenticated actions. OWASP treats that as defense in depth, not a replacement, since it does nothing for unauthenticated pages. In short: a real but conditional weakness, cheap to fix, rarely the most urgent item in a report.
How to confirm it on the host
Start with the exact URLs from the plugin output. Send a GET request and print only the headers you care about:
curl -s -D - -o /dev/null https://app.example.com/login | grep -iE '^(x-frame-options|content-security-policy):'
The colon at the end of the pattern matters: without it, a Content-Security-Policy-Report-Only header would also match, and a report-only policy does not enforce frame-ancestors. Repeat for an error page and a plain HTTP redirect, because those responses are where the header most often goes missing:
curl -s -D - -o /dev/null https://app.example.com/does-not-exist | grep -iE '^(x-frame-options|content-security-policy):'
curl -s -D - -o /dev/null http://app.example.com/ | grep -iE '^(x-frame-options|content-security-policy):'
If the scan targeted an IP address rather than a hostname, test the IP as well (-k turns off certificate verification, which is needed because the certificate will not match the IP), since it may land on a different virtual host:
curl -sk -D - -o /dev/null https://203.0.113.10/ | grep -iE '^(x-frame-options|content-security-policy):'
These commands assume a POSIX shell. On Windows, call curl.exe explicitly (in Windows PowerShell 5.1, curl is an alias for Invoke-WebRequest), write the body to NUL and filter with Select-String instead of grep:
curl.exe -s -D - -o NUL https://app.example.com/login | Select-String -Pattern '^(x-frame-options|content-security-policy):'
Invoke-WebRequest also works, but in Windows PowerShell 5.1 it throws on 4xx and 5xx responses and follows redirects, so it only checks pages that return 200:
$r = Invoke-WebRequest -Uri https://app.example.com/login -UseBasicParsing
$r.Headers['X-Frame-Options']
$r.Headers['Content-Security-Policy']
In PowerShell 7 and later, add -SkipHttpErrorCheck -MaximumRedirection 0 to the same command to inspect error and redirect responses.
Empty output confirms the finding. Also note whether a header appears twice, which usually means two layers (proxy and application) are both setting it.
How to fix it: the clickjacking X-Frame-Options fix per web server
Choose the value before touching configuration:
| Header value | Effect | Use when |
|---|---|---|
| X-Frame-Options: DENY | No framing at all, including by your own site | Nothing ever frames the application |
| X-Frame-Options: SAMEORIGIN | Only pages from the same origin may frame it | Default choice for most applications |
| Content-Security-Policy: frame-ancestors ‘self’ | CSP equivalent of SAMEORIGIN, and it takes precedence over X-Frame-Options in browsers that support it | Always, alongside X-Frame-Options |
| Content-Security-Policy: frame-ancestors ‘self’ https://portal.example.com | Allows specific trusted origins | A known portal or partner must embed the page |
Do not use ALLOW-FROM: it is obsolete and modern browsers ignore it. Neither header works when placed in an HTML <meta> tag; both must be real HTTP response headers.
Apache HTTP Server
The Header directive needs mod_headers (on Debian and Ubuntu, enable it with a2enmod headers). Add these lines to the virtual host or server config:
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"
The always condition makes Apache add the header to non-2xx responses and keep it across internal redirects such as ErrorDocument handlers. If a proxied or FastCGI backend already sends X-Frame-Options, the Apache documentation recommends clearing it from the default table first to avoid a duplicate:
Header onsuccess unset X-Frame-Options
Header always set X-Frame-Options "SAMEORIGIN"
If the application already sends its own Content-Security-Policy, do not overwrite it with Header set; add frame-ancestors 'self' to the existing policy at its source. Then test and reload:
apachectl configtest
sudo systemctl reload apache2 # the unit is httpd on RHEL-family systems
nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
The always parameter (nginx 1.7.5 and later) adds the header regardless of status code; without it, error responses go out bare. Watch the inheritance rule: add_header directives are inherited from the previous level only if the current level defines none. A location block with its own add_header (for Cache-Control, say) silently drops your server-level headers. Repeat them in such blocks, keep them in an included snippet, or on nginx 1.29.3 and later use add_header_inherit merge;. If the upstream application already sets X-Frame-Options, proxy_hide_header X-Frame-Options; in the proxied location prevents two copies.
sudo nginx -t && sudo systemctl reload nginx
IIS
Add a customHeaders entry in the site’s web.config (or in applicationHost.config for the whole server):
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="SAMEORIGIN" />
<add name="Content-Security-Policy" value="frame-ancestors 'self'" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
The same X-Frame-Options entry can be added from an elevated prompt with appcmd:
%windir%system32inetsrvappcmd.exe set config "Default Web Site" -section:system.webServer/httpProtocol /+"customHeaders.[name='X-Frame-Options',value='SAMEORIGIN']"
In IIS Manager the path is: select the site, open HTTP Response Headers, click Add. Custom headers are returned in every response. If the application framework also sets X-Frame-Options, keep only one source.
Apache Tomcat
Enable the built-in HttpHeaderSecurityFilter in the application’s WEB-INF/web.xml, or in $CATALINA_BASE/conf/web.xml to cover every deployed application:
<filter>
<filter-name>httpHeaderSecurity</filter-name>
<filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>antiClickJackingOption</param-name>
<param-value>SAMEORIGIN</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>httpHeaderSecurity</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
antiClickJackingEnabled defaults to true and antiClickJackingOption defaults to DENY. The same filter also enables Strict-Transport-Security on HTTPS requests (with hstsMaxAgeSeconds defaulting to 0) and X-Content-Type-Options: nosniff, so add an hstsEnabled init-param set to false if HSTS is managed elsewhere. The filter does not send CSP; add frame-ancestors at the reverse proxy or in the application. Restart Tomcat or redeploy.
Load balancers, CDNs and WAFs
The scanner sees the headers of whatever answers first. If a CDN, reverse proxy or WAF terminates the connection, set the header there or confirm the origin’s header passes through (see how a WAF changes what your web scanner reports). Setting it at both edge and origin is the usual cause of duplicates.
How to verify the fix and rescan
Rerun the curl commands above against the plugin’s URLs, an error page and a redirect. Each should return exactly one X-Frame-Options header and an enforced Content-Security-Policy header containing frame-ancestors. A frame-ancestors directive that appears only in Content-Security-Policy-Report-Only does not count, because report-only policies are not enforced. For a browser check, save this as a local HTML file and open it; the frame should stay blank and the developer console should report that the page refused to be framed:
<iframe src="https://app.example.com/login" width="800" height="600"></iframe>
Then rerun the Nessus scan that includes plugin 85582 against the same targets and ports, or rescan the application in Tenable WAS. Header findings are runtime observations, the kind only a dynamic scan produces (see what SAST, DAST and SCA scanners can each see), so a code review alone will not close them. Compare the new URL list against the old one, not just the count.
What can break and how to roll back
- Legitimate embedding: intranet portals or other applications that frame your pages from a different origin show a blank frame and a console error. Use
frame-ancestorswith the specific origins; browsers that support CSP ignore X-Frame-Options when frame-ancestors is present, so SAMEORIGIN can stay as the fallback. - DENY on self-framing apps: CMS preview panes and admin UIs that iframe their own pages break under DENY. Use SAMEORIGIN.
- Overwritten CSP:
Header set Content-Security-Policyreplaces a policy the application already sends, which can remove its script restrictions. - nginx inheritance: adding
add_headerinside one location drops every server-level header there, including HSTS.
Rollback is immediate because only headers change. Back up the config first; to revert, remove the lines, run the config test and reload. On IIS restore the web.config backup or remove the entry under HTTP Response Headers; on Tomcat remove the filter and restart.
Common false positive reasons
Tenable notes that plugin 85582 may produce false positives when other mitigations such as frame-busting JavaScript are in place, or when the page performs no security-sensitive transactions. Before disputing the finding, rule out these cases:
- Non-HTML responses: images, downloads and JSON endpoints do not need framing protection; document them as accepted.
- Frame-busting scripts only: treat these as a legacy measure and add the headers anyway.
- Meta tag instead of a header: not a false positive, since browsers ignore both directives in
<meta>. - Different virtual host: scanning by IP often reaches a default site you did not configure.
- Success-only headers: without
always, error pages (and on Apache also redirects) lack the header, so the scanner is right. - Stale results: the scan ran before the reload, or a CDN served cached headers.
FAQ
Is X-Frame-Options deprecated now that CSP frame-ancestors exists?
Only the ALLOW-FROM value is obsolete. When both are present, browsers that support CSP use frame-ancestors and ignore X-Frame-Options. Send both: scanners check for X-Frame-Options, and older clients still rely on it.
Should I use DENY or SAMEORIGIN?
DENY if nothing, including your own site, ever frames the page. SAMEORIGIN for everything else, which is the safer default for most applications.
Can I add the header with an HTML meta tag?
No. Both X-Frame-Options and frame-ancestors are ignored in a meta element. They must be sent as HTTP response headers.
Does every response need the header?
Every HTML response does. Setting it globally with always is simpler than tracking which paths return HTML.
Tracking this finding across many hosts
A missing header tends to show up on dozens of virtual hosts at once, and the hard part is knowing which ones are really closed. SITEY, a self-hosted vulnerability management platform, imports findings from 16 scanners (Nessus results arrive as an uploaded .nessus export) and merges duplicates within each scanner, not across scanners, so expect separate records for Nessus and web app scanner findings on the same site. For Nessus findings it can run a per-finding Nessus retest and close the item only when the plugin no longer fires.
Sources
- Tenable: Nessus plugin 85582, Web Application Potentially Vulnerable to Clickjacking
- Tenable: WAS plugin 98060, Missing ‘X-Frame-Options’ Header
- OWASP Clickjacking Defense Cheat Sheet
- nginx: ngx_http_headers_module (add_header)
- Apache Tomcat 10.1: Container Provided Filters (HTTP Header Security Filter)