Part of the guide: Website Security on Shared Hosting: Practical Guides

Website Security Checklist: 16 cPanel Checks (2026)

By Published 19 min read Security
Website Security Checklist: 16 cPanel Checks (2026)

HTTP/2 200 on /backup.zip, /.env or an internal PHP endpoint can mean your server is exposing something that should never be public. If the response contains sensitive data, you have a security incident to investigate, not a configuration tweak.

Automated scanners don't need to know your website or its CMS. They request predictable paths, read response headers, test old software and look for weak access controls. A practical website security checklist should follow the same probes, starting with the mistakes that can expose credentials or application data.

To secure a cPanel website:

  1. Check for exposed configuration files, backups and database dumps.
  2. Disable directory indexing.
  3. Verify HTTPS and certificate renewal.
  4. Harden response headers and session cookies.
  5. Update outdated libraries.
  6. Inspect WordPress endpoints.
  7. Protect internal PHP scripts.
  8. Test server configuration and admin-only actions.

Repeat these checks after every deployment and significant update.

Why automated scanners find mistakes before you do

The first minute of an opportunistic attack can be entirely automated. A bot can request dozens of known filenames and endpoints without understanding your application. One missing authentication check, or one public backup, is enough to cause a serious problem.

Not every scanner runs the same sequence. This website security checklist follows a risk-first order based on common automated probes.

I saw how easy it is to overlook application-specific problems on October 23, 2026. I ran a read-only probe against about 50 URLs on headsntail.com. Sensitive files were protected, but three findings surprised me:

  • Internal PHP files, including track.php and header.php, responded to direct requests.
  • An owner cookie I used to exclude my own visits from analytics, and to trigger cache maintenance, had the fixed value 1.
  • The CSP frame-ancestors and Permissions-Policy response headers were missing.

I fixed all three that day. The HeadsNTail SEO audit then reported a Security score of 100/100. That score confirmed the checks the audit covers, not that every possible vulnerability was gone.

Only run the probes below against sites you own or are authorized to test.

Website security checklist part 1: exposed files (checks 1–7)

1. Check whether .env files are publicly accessible

Laravel and other PHP applications often store credentials in .env. A successful request can expose database passwords, mail credentials and the application key.

Test:

curl -I https://example.com/.env
curl -I https://example.com/.env.production

Expected result: 403 Forbidden or 404 Not Found.

A 200 OK needs a closer look. Many applications send their normal page for unknown URLs (a soft 404), so check what actually came back. This prints only the first 200 bytes:

curl -s --max-time 10 https://example.com/.env | head -c 200
  • If you see lines like APP_KEY= or DB_PASSWORD=: the file is exposed. Block it now and rotate every secret it contains: database password, mail password, API keys and APP_KEY. Blocking access doesn't undo a leak that already happened.
  • If you see your site's HTML: it's a soft 404, not a leak.

For Laravel, the safest deployment keeps the application outside public_html and exposes only the public directory. See the Laravel deployment checklist for cPanel for the correct document-root layout.

2. Block access to .git directories

An exposed .git directory can reveal source code, repository history and old configuration, including secrets you later deleted.

Test:

curl -I https://example.com/.git/config
curl -I https://example.com/.git/HEAD

Don't rely on disabled directory indexing here. It hides the folder listing but leaves individual Git files downloadable by their known names. Tools exist that rebuild a whole repository from those files.

Keep repositories outside the public document root, and explicitly deny .git requests (rule below).

3. Remove phpinfo.php from production

A forgotten phpinfo.php can disclose loaded extensions, environment variables, filesystem paths and PHP configuration.

Test:

curl -I https://example.com/phpinfo.php
curl -I https://example.com/info.php

Delete the diagnostic file after you're done with it. A password-protected diagnostic page is safer than a public one, but deleting it is better.

4. Find old configuration backups

These files are easy to overlook:

  • config.php.bak
  • config.php.old
  • config.php~
  • .env.backup

The original config.php runs as PHP and outputs nothing. A copy with a different extension isn't executed, so the server returns its source code, credentials included, as plain text.

Test:

curl -I https://example.com/config.php.bak
curl -I https://example.com/config.php~

Don't keep backup copies next to production configuration files. Editors also leave .swp and ~ files behind when you edit over SSH.

5. Search for downloadable database dumps

A database export inside public_html may be downloadable without authentication.

Check predictable names:

curl -I https://example.com/database.sql
curl -I https://example.com/backup.sql
curl -I https://example.com/db.sql.gz

A dump can contain account records, password hashes, personal information and operational data. Store exports outside the web-accessible directory.

6. Check for public ZIP archives

Developers sometimes upload a complete website archive during a migration, then forget to remove it.

Test:

curl -I https://example.com/backup.zip
curl -I https://example.com/site-backup.zip

A public archive exposes far more than a single configuration file: every file, every .env and often a database dump too.

Use protected backup storage instead. The cPanel automatic backup to Google Drive guide covers a setup that never leaves downloadable archives in the web root.

7. Look for error logs and hidden operating-system files

Check:

curl -I https://example.com/error_log
curl -I https://example.com/.DS_Store

An accessible error_log can reveal paths, SQL errors and application internals. .DS_Store files, created by macOS, can list directory and file names you never linked to. Neither belongs in a public deployment.

Block exposed files with .htaccess

On Apache 2.4 hosting, place this block near the top of the document-root .htaccess, before the application's catch-all rewrite:

Options -Indexes

<IfModule mod_rewrite.c>
    RewriteEngine On

    # Deny .git anywhere in the path
    RewriteRule (^|/)\.git(/|$) - [F,NC]
</IfModule>

<FilesMatch "(?i)(^\.env(\..*)?$|^phpinfo\.php$|^info\.php$|^error_log$|^\.DS_Store$|^(backup|site-backup|www|public_html)\.(zip|tar|tar\.gz|tgz|rar|7z)$|(\.bak|\.old|\.orig|\.save|\.swp|\.sql|\.sql\.gz|~)$)">
    Require all denied
</FilesMatch>

FilesMatch checks the file name only, so these rules also apply inside subfolders.

This blocks common names. It doesn't replace looking at what's actually in your hosting account, because an archive called june-final.zip won't match. Don't simply deny every .zip file, though: that also breaks legitimate downloads such as brochures or plugin packages.

To generate a starting .htaccess for your setup, use the .htaccess generator.

Scan all the paths at once

base="https://example.com"

for path in \
  .env \
  .git/config \
  phpinfo.php \
  config.php.bak \
  config.php~ \
  database.sql \
  backup.zip \
  error_log \
  .DS_Store
do
  printf "%-24s " "$path"

  curl -sS -I \
    --max-time 10 \
    -o /dev/null \
    -w "%{http_code}  %{content_type}\n" \
    "$base/$path"
done

How to read the output:

  • 403 or 404: fine.
  • 200 with text/html: usually a soft 404 (your normal page). Confirm with the head -c 200 command from check 1.
  • 200 with application/octet-stream, text/plain, application/zip or application/sql: treat it as exposed until proven otherwise.
  • 405 on every path: the server rejects HEAD requests. Use the head -c 200 GET for the paths that matter.

These requests use HEAD, so the script doesn't download file contents.

When you investigate, keep the response on your own machine. Never paste exposed credentials or backup contents into an online testing service or chat tool.

If a rule produces 500 Internal Server Error, your host doesn't allow that directive in .htaccess. See the troubleshooting section below.

Website security checklist part 2: server and browser hardening (checks 8–12)

8. Disable directory listings in uploads and asset folders

Try visiting:

https://example.com/uploads/
https://example.com/assets/
https://example.com/storage/

If Apache shows an Index of /uploads page, directory listing is enabled. The Options -Indexes line above turns it off for the whole document root.

Upload directories deserve particular attention because they contain files supplied by users.

Disabling indexing doesn't stop anyone from requesting a filename they already know. Sensitive uploads such as invoices or ID documents should live outside the public directory, or be served through an authenticated application route.

Also stop PHP from running inside upload folders. A .htaccess line written for mod_php (like php_flag engine off) does nothing under PHP-FPM, or causes a 500 error. This rule works with any PHP handler because it blocks the request before PHP is reached. Put it in uploads/.htaccess:

<FilesMatch "(?i)\.(php[0-9]?|phtml|phar)$">
    Require all denied
</FilesMatch>

Test it by uploading a harmless test.php containing <?php echo 'x'; and requesting it. You should get 403. Then delete the test file.

9. Verify HTTPS certificates and automatic renewal

A trusted HTTPS connection needs a valid certificate, the correct hostname and a complete certificate chain.

Start with:

curl -Iv https://example.com/

For certificate dates and issuer:

echo | openssl s_client \
  -connect example.com:443 \
  -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -issuer

That shows certificate details but doesn't validate the chain. This one does:

echo | openssl s_client \
  -connect example.com:443 \
  -servername example.com \
  -verify_return_error 2>&1 \
  | grep "Verify return code"

You want Verify return code: 0 (ok). Any other code (for example 21 (unable to verify the first certificate)) usually means a missing intermediate certificate. Desktop browsers sometimes hide that problem; API clients and some mobile apps don't.

In cPanel, open SSL/TLS Status and confirm that AutoSSL covers every domain and subdomain you use, including www.

Certificate lifetimes are getting shorter:

  • Let's Encrypt has issued 90-day certificates since its 2015 launch.
  • Its optional 6-day certificates became generally available in January 2026.
  • The default drops to 64 days on February 10, 2027, and to 45 days on February 16, 2028.
  • Separately, industry rules cap public TLS certificates at 47 days from March 15, 2029.

The practical result: automatic renewal is no longer optional. Check that renewal works before expiry instead of waiting for a browser warning.

10. Configure eight HTTP header protections

Security scanners often report missing HTTP response headers. This Apache block sets six browser protections and removes two information-disclosure headers:

<IfModule mod_headers.c>

    Header always set X-Content-Type-Options "nosniff"

    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    Header always set X-Frame-Options "SAMEORIGIN"

    Header always set Content-Security-Policy "frame-ancestors 'self'"

    Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"

    Header always set Strict-Transport-Security "max-age=300" "expr=%{HTTPS} == 'on'"

    Header always unset X-Powered-By
    Header unset X-Powered-By

    Header always unset Server
    Header unset Server

</IfModule>

Both unset forms are there on purpose. PHP adds X-Powered-By to a different internal header table than Apache's own error responses, and each form clears one of them.

The eight items are:

  1. Strict-Transport-Security
  2. Content-Security-Policy
  3. X-Content-Type-Options
  4. Referrer-Policy
  5. Permissions-Policy
  6. X-Frame-Options
  7. Removal of X-Powered-By
  8. Removal or reduction of Server information

The cleanest fix for X-Powered-By is at the source. Set expose_php = Off in cPanel's MultiPHP INI Editor, and PHP stops sending it at all.

There is one server-level limitation. Apache adds its own Server header after .htaccess processing, so the unset lines often can't remove it. ServerTokens needs server-level access, so ask your host to reduce that information if it matters to you.

Verify the actual responses:

curl -I https://example.com/
curl -I https://example.com/missing-page-test

Check both the normal and the error response. Headers set with always also apply to error responses; plain set doesn't.

HSTS rollout:

  1. Start at max-age=300, as above.
  2. Once HTTPS has worked consistently for a while, raise it to 31536000 or more.
  3. Add includeSubDomains only when every affected subdomain supports HTTPS.
  4. Don't enable HSTS preload unless you understand that removing a domain from the preload list takes months.

CSP: don't copy an aggressive Content-Security-Policy from another site. A policy that restricts script-src or frame-src can block AdSense, embedded media and payment widgets. The narrow frame-ancestors directive above only controls who can embed your pages, so it doesn't affect ad scripts your pages load.

For deeper configuration details, read the cPanel security headers .htaccess guide.

11. Secure PHP session cookies

A PHP session cookie should normally have:

  • Secure, so it is only sent over HTTPS.
  • HttpOnly, so JavaScript can't read it.
  • SameSite=Lax, to limit sending it on cross-site requests.

In cPanel's MultiPHP INI Editor, or in php.ini:

session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax
session.use_strict_mode = 1

If the editor doesn't offer these settings and your site runs PHP-FPM, put the same lines in a .user.ini file in the document root. PHP rereads .user.ini periodically (every 300 seconds by default), so changes aren't instant.

Laravel ignores these php.ini settings because it manages its own session cookie. Set them in config/session.php and .env instead:

SESSION_SECURE_COOKIE=true

In config/session.php, keep 'http_only' => true and 'same_site' => 'lax'. Then run php artisan config:clear, or php artisan config:cache if you cache configuration in production.

Test:

curl -sI https://example.com/login | grep -i "set-cookie"

Every session cookie line should include secure, HttpOnly and SameSite=Lax.

An anonymous informational page shouldn't create a PHP session unless the application needs one. Some applications do need anonymous sessions for CSRF protection or similar workflows, so don't disable sessions blindly. Cross-site sign-in flows may also require a different SameSite value.

Running an outdated PHP version undoes much of this work. See the PHP security update guide for cPanel and XAMPP.

12. Identify outdated JavaScript libraries

An old frontend dependency can stay in production long after the backend has been patched.

Minified jQuery and Bootstrap files start with a version banner. Search for those banners over a local or SSH-accessible copy of your site:

grep -RhoE \
  "jQuery v[0-9]+\.[0-9]+\.[0-9]+|Bootstrap v[0-9]+\.[0-9]+\.[0-9]+" \
  --include="*.js" \
  public_html/ | sort | uniq -c

Then check what the live page actually loads. WordPress and many themes add the version as a query string:

curl -s https://example.com/ \
  | grep -oiE "(jquery|bootstrap)[^\"']*\.js(\?ver=[^\"']*)?"

If the two disagree, a CDN copy or a plugin is loading its own version.

jQuery support status:

  • jQuery 4.0.0 was released in January 2026, and 4.x is now the current branch.
  • jQuery 3.x receives only critical security patches and bug fixes.
  • jQuery 2.x ended at 2.2.4 and 1.x at 1.12.4, both released May 20, 2016. Neither receives patches.

For Bootstrap, compare the installed version against the official security advisories.

Don't swap an old library directly on a busy production site without testing. Major upgrades can break plugins, themes and your own JavaScript. jQuery provides the jQuery Migrate plugin to show what breaks before you commit.

Website security checklist part 3: application and admin checks (checks 13–16)

13. Reduce CMS fingerprints and check WordPress exposure

Automated scanners identify CMS installations by predictable files and endpoints:

/readme.html
/license.txt
/wp-json/
/xmlrpc.php

Also look for generator metadata and version query strings in the generated HTML.

Hiding version information is useful housekeeping, but it doesn't patch a vulnerable installation.

For WordPress, check whether XML-RPC is actually needed. Disabling it can affect the mobile app, remote publishing and some integrations such as Jetpack.

Review the REST API user endpoint:

curl -s https://example.com/wp-json/wp/v2/users | head -c 500

Public author names and slugs aren't automatically a vulnerability. Email addresses, administrator-only accounts, or users who never publish posts appearing there are a reason to review configuration.

Plugin maintenance matters more than cosmetic version hiding. According to Patchstack's State of WordPress Security in 2026:

  • The WordPress ecosystem recorded 11,334 new vulnerabilities in 2025, up 42% from 2024.
  • 91% were in plugins and 9% in themes; just six were in WordPress core.
  • 1,966 (17%) were high severity.
  • 46% had no developer fix available when publicly disclosed.
  • Broken Access Control was the most exploited vulnerability type.

The useful habit: review plugin updates, remove unused extensions (deactivated plugins can still be attacked if their files stay on the server), and check permissions on sensitive actions.

14. Prevent direct access to internal PHP handlers

This is the problem my own October 23 probe exposed: files such as header.php, track.php and internal API handlers responded when requested directly by URL.

A direct response doesn't automatically mean a file is exploitable. It means you should confirm whether direct access was intended, and whether the script performs sensitive operations without the checks the rest of the application applies.

For files that should never be requested directly, an Apache rule can return 404:

<IfModule mod_rewrite.c>
    RewriteEngine On

    # Matches header.php / track.php in this folder and any subfolder
    RewriteRule (^|/)(track|header)\.php$ - [R=404,L,NC]
</IfModule>

Place it before broad front-controller rewrites and adapt the filenames to your application. Without the (^|/) prefix, the rule only matches files in the same folder as the .htaccess, which is a common reason it "doesn't work" for includes/header.php.

A guard inside the file itself works on any server, including Nginx and LiteSpeed, and survives .htaccess being overwritten. Put it at the top of each include-only file:

<?php
// Return 404 when this file is requested directly instead of included
if (realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
    http_response_code(404);
    exit;
}

For Laravel, keep private PHP code outside the public document root and route web requests through public/index.php.

Don't block a real webhook or public API endpoint just because it's a PHP file. Authenticate, authorize and validate those requests instead.

15. Test server-status, TRACE and CORS

Three server-level issues are easy to miss.

1. Apache status page. Check whether it's exposed:

curl -i https://example.com/server-status

Expected: 403 or 404. A page showing client IPs and requested URLs is a leak.

2. TRACE. Check whether it's enabled:

curl -i -X TRACE https://example.com/

Expected: 405 Method Not Allowed (or 403/501). TRACE normally shouldn't be enabled on a public production website.

3. CORS. Test how the application handles an untrusted cross-origin request:

curl -i \
  -H "Origin: https://untrusted.example" \
  https://example.com/api/status

Look at Access-Control-Allow-Origin and Access-Control-Allow-Credentials. The dangerous combination is your untrusted origin echoed back together with Access-Control-Allow-Credentials: true. That lets any website read a logged-in user's responses from your API.

CORS isn't authentication. Avoid reflecting arbitrary origins, especially on authenticated endpoints.

On managed cPanel hosting, fixing TRACE or server-status exposure usually requires the hosting provider.

16. Protect owner shortcuts and maintenance controls

This was the most interesting finding in my own audit. The site had an owner cookie with the fixed value 1. It marked owner traffic for analytics and was tied to page-cache maintenance actions.

Anyone can set a cookie to 1 in their browser's developer tools, so an easily guessed value is not authentication.

I changed it so the dashboard generates a random owner key, writes it to the site, and compares the provided value with PHP's hash_equals(). Cache purge and warm operations are also rate-limited now.

The same pattern in a few lines. Generate the key once:

php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"

Store it outside the web root (for example as OWNER_KEY in .env), then compare in constant time:

<?php
$expected = getenv('OWNER_KEY') ?: '';
$given    = $_COOKIE['owner_key'] ?? '';

$isOwner = $expected !== ''
    && is_string($given)
    && hash_equals($expected, $given);

Set that cookie with Secure and HttpOnly. A secret that JavaScript can read is easy to steal with a single XSS bug.

The principle applies to any maintenance endpoint:

  • Use cryptographically random secrets or proper authenticated sessions.
  • Check authorization before running the action.
  • Rate-limit sensitive operations.
  • Never trust a query parameter or cookie just because it holds an expected static value.

For a new application, prefer authenticated POST requests with CSRF protection for browser-based maintenance actions. A secret cookie on its own shouldn't replace real authentication and authorization.

This is exactly the kind of issue a header scanner won't find.

Run the free 10-minute website security audit

A short repeatable routine catches more configuration mistakes than an occasional manual inspection.

  1. HeadsNTail SEO audit: run its security scan alongside the technical SEO checks. Its checks now include the issues from my October 23 audit.
  2. SecurityHeaders.com: inspect missing or misconfigured response headers.
  3. Mozilla Observatory: review browser-facing security configuration and recommended improvements.
  4. The curl checks above: run them against infrastructure you own or are authorized to test.

To repeat the path scan automatically, save it as a script and schedule it weekly. The cron job generator builds the schedule line.

Fix findings in this order:

  1. Exposed credentials and backups
  2. Unauthorized access to internal endpoints
  3. Weak administrative controls
  4. Vulnerable software
  5. TLS problems
  6. Missing browser protections

A missing header is worth fixing, but a downloadable database backup is an emergency.

If you want help investigating a finding, contact HeadsNTail.

When .htaccess security changes cause problems

The site shows 500 Internal Server Error after adding rules

Your host doesn't allow one of the directives in .htaccess. Each one needs a matching AllowOverride permission:

  • Options -Indexes needs AllowOverride Options (or Options=Indexes).
  • RewriteRule and Header need AllowOverride FileInfo.
  • Require all denied needs AllowOverride AuthConfig.

Remove the blocks one at a time to find the one that fails, then ask your host which override classes are enabled. Check the account's error_log: it usually names the directive that isn't allowed.

FilesMatch rules don't appear to work

First check which server answers the request: Apache, LiteSpeed, or Nginx in front of Apache. Run curl -I and look at the Server header.

  • LiteSpeed reads most Apache .htaccess syntax.
  • Nginx serving static files directly ignores .htaccess completely. Requests for existing files such as backup.zip may never reach Apache.

A cPanel account can't override every server-level setting.

The same response header appears twice

Your application, .htaccess, CDN or reverse proxy may each add the same header. Inspect the final public response rather than assuming one configuration file controls everything.

For a duplicated CSP or HSTS policy, pick the layer that should own it and remove the other definitions. Browsers enforce every CSP they receive, so two policies combine into the stricter one.

HSTS breaks a subdomain

This usually happens when includeSubDomains is enabled before every subdomain supports HTTPS.

Start with a small max-age, test carefully and increase it only after checking all affected services. Browsers cache the HSTS policy, so a visitor who already received a long max-age stays locked to HTTPS until it expires.

The website breaks after enabling CSP

Open the browser developer tools console and look for CSP violation messages. A restrictive script-src, connect-src or frame-src can block advertising, analytics and embedded services.

Start with the narrow protections you understand. Test a broader policy with the Content-Security-Policy-Report-Only header before enforcing it.

ACME or .well-known returns 403

Certificate validation (AutoSSL and Let's Encrypt) depends on requests to:

/.well-known/acme-challenge/

Don't block every dot-prefixed path with a blanket rule such as RewriteRule (^|/)\. - [F]. That also blocks .well-known, and renewals fail quietly until the certificate expires. The rules in this checklist name specific files and folders for exactly this reason.

Also check that your CDN or redirect configuration doesn't interfere with the challenge.

FAQ

Is a WordPress security plugin enough?

No. A plugin can help with login protection, malware detection and vulnerability alerts, but it can't guarantee that backups, .git directories or server configuration are protected. Files like backup.zip in the web root are served by the web server before WordPress even loads. You still need to audit the server, the PHP configuration and the public files.

Does Cloudflare replace server-side security?

No. Cloudflare can filter requests and add useful protections, but your origin server still needs correct authentication, file permissions and configuration. If the origin IP is known, or a subdomain bypasses the proxy, attackers can reach the server directly and skip every Cloudflare rule.

How often should I run a website security checklist?

Run it after deployments, configuration changes, plugin updates and hosting migrations. For actively maintained websites, a weekly automated check that alerts on changes is a practical starting point. Handle critical exposures immediately rather than waiting for the next scheduled audit.

Does website security affect Google SEO?

HTTPS is a lightweight Google ranking signal, but adding security headers doesn't improve rankings by itself. A compromised site can lose rankings fast, though: Google can show "This site may be hacked" warnings, and injected spam pages hurt search visibility. Badly configured CSP rules can also stop ads or analytics from loading, so test those after every security change.

Keep this website security checklist in your deployment process. Your next step: run a read-only scan of your own production domain before changing its configuration.

Free tools for Security work

All free tools →

More Security articles