An index of website security features
November 24th, 2022I recently took a bit of dive into website security. As the developer of a website, of course I'd like my website to be secure, even if it doesn't handle any sensitive user data. It's also just a fun way to learn more about how the web we use every day works. I read about and implemented a number of security features that I gathered from various blogs, lists, and validation tools, and I wanted to collect them in one spot for my own reference and for anyone else who might find them useful.
I'll include a few words about what each of them do, but Google will get you all the in-depth information you need, so this is mostly intended as an index of some features that exist so you can look into them more.
Example code is in PHP.
This list is certainly not exhaustive. Here are a few other sources I referenced:
CSP (Content Security Policy)
The Content Security Policy is an HTTP header that decribes what types of media, scripts, styles, and other content your page will have on it. This allows the user's browser to block any unexpected content that may have somehow been injected by an attacker (maybe in a forum post, or via an XSS attack).
Every HTML page served should have a CSP. My workflow for adding them is to add the following maximally-restrictive CSP, load the page in a browser, check the developer console for CSP errors, and add entries to permit those things. For embedded styles or scripts, the console will report a hash you can add to the CSP.
header("Content-Security-Policy: "
."default-src 'none';"
."frame-ancestors 'none';"
."base-uri 'none';"
."form-action 'none';");
You cannot use inline styles or Javascript events with a CSP without adding the 'unsafe-inline' item, which defeats much of the purpose of having a CSP.
Mitigates: XSS, clickjacking
SSL (HTTPS)
Most other security measures can be circumvented if an attacker can intercept the communications between a user and the server (such as via ARP poisoning). Acquiring an SSL certificate and using the HTTPS protocol will allow your site to use encrypted communications, making this impossible. Many hosting providers are now providing ways to acquire free certificates
You must also redirect insecure HTTP requests to HTTPS, such as via this htaccess rule:
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Strict-Transport-Security (HSTS)
Now that your site supports HTTPS, add the Strict-Transport-Security: max-age=<expire-time> header to every file served. This directs the user's browser to only use HTTPS when connecting to your site. This performs a slightly different function than a server-side 301 redirect to HTTPS, in that the browser itself will upgrade future requests to HTTPS before ever sending them to the server.
Via htaccess:
Header set Strict-Transport-Security "max-age=31536000"
Mitigates: man-in-the-middle attacks
X-Frame-Options: deny
Add the X-Frame-Options: deny header to every HTML page served. This header instructs the browser to not permit your page to be framed on another page.
Via htaccess:
<filesMatch ".(php|html|htm)$">
Header set X-Frame-Options "deny"
</filesMatch>
Mitigates: clickjacking
X-Content-Type-Options: nosniff
Add the X-Content-Type-Options: nosniff header to every file served. In some cases, browsers will ignore the MIME type set by the server and try to determine it by looking at the file. This can be a security risk if your site hosts user-provided files, which could have a harmless extension such as .txt but be interpretted by browsers as executable code such as Javascript.
Header set X-Content-Type-Options "nosniff"
Mitigates: MIME confusion
Remove X-Powered-By
Some web technologies, such as PHP, will identify themselves in the X-Powered-By header of responses. This could help an attacker find weaknesses in the site to attack. Remove this header from all responses.
Header unset X-Powered-By
Mitigates: fingerprinting
MySQL Prepared Statements
Always use prepared statements to make dynamic SQL queries. Forming queries by concatenating strings may allow an attacker to perform SQL injection, if user-provided data can find its way into any of the components in the query. The attacker might be able to extract private data from tables, insert malicious data, or vandalize or drop data.
$query = $connection->prepare("INSERT INTO blog (id, date, title, text) VALUES (NULL, NOW(), ?, ?)");
$query->bind_param("ss", $unsafeTitle, $unsafeText);
$query->execute();
Mitigates: SQL injection
Data sanitization
Sanitize user-provided data as necessary before using it in any context. Use a function like PHP's strip_tags for text. If you expect data to be a number or bool, run it through intval, floatval, or boolval. This will prevent an attacker from inserting malicious code into pages (XSS attack).
I like to name any user-provided variable that hasn't been thoroughly sanitized yet with the prefix "unsafe". This helps me remember that that data may need to be sanitized before being used.
This applies equally to data that was previously provided by a user and stored ("second-order attack").
Mitigates: XSS, SQL injection
Hash Passwords
Passwords on the server should always be stored as hashes; the original password should not be present on the server. This will prevent an attacker who finds a way to extract data from the server from logging in with those passwords (without cracking the hashes).
In PHP, you can generate a salted password hash like so (either offline or online depending on your needs):
$passwordHash = password_hash("PASSWORD", PASSWORD_DEFAULT);
And check it like so:
$unsafePassword = $_POST['pword'];
if (password_verify($unsafePassword, 'PASSWORD_HASH'))
{
// Restricted code
}
Mitigates: password leakage
Restrict MySQL user privileges
Scripts generating pages should use a MySQL user that has only the privileges it needs (for example, only the SELECT privilege on tables containing public data). This will prevent an attacker who finds an injection vulnerability from using it to modify or delete data.
Mitigates: SQL injection
Files on your server are public
Even if there are no links to them, any file on your server that isn't protected by an actual access control mechanism should be considered public. Do not upload .git folders, documents containing passwords, pre-minified source, etc., or an attacker might be able to enumerate and download them. Obscurity is not security.
Mitigates: data leakage, fingerprinting
Use rel="noopener" on external links that open in new tabs
When a user follows a link that opens a new tab, older browsers may provide the new page with the window.opener property, a reference to your page's window. The target page could use this to do certain operations on your page (in particular, navigate to a different URL).
Browsers will restrict this by default since late 2018, but you can be sure it's done by providing rel="noopener" on any external target="_blank" links.
<a href="https://example.com" target="_blank" rel="noopener">Link</a>
Mitigates: phishing attacks
Subresource Integrity
If your site loads resources such as scripts or stylesheets from an external domain such as CloudFlare or Google Fonts, how do you know the provider (or an attacker) hasn't inserted something malicious into them? The answer is to add a hash of the file's expected contents to the integrity parameter on the relevant HTML tag. The browser will refuse to use the content if the hash doesn't match the file served.
<script src="https://example.com/example-framework.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin>
</script>
Mitigates: supply-chain compromise
Use captchas before sending emails
This is a weird one that I learned about when it actually happened on my site. If your site accepts e-mail addresses from users, you must perform a captcha on the user before sending any mail to verify they are not a bot.
Why? If an attacker has compromised a user account on some other site (such as a bank) and intends to perform some action that might send the user an e-mail alert, they can have a bot automatically feed that user's e-mail address to any site with a form that will take it. The intent is to flood that inbox with e-mails in the hope that the user will not notice the important one. This is basically a kind of DDoS attack.
This isn't just bad for the target: it could also land your domain on a spam blacklist, which can be difficult to fix.
Google provides a free, low-friction captcha called reCAPTCHA.
Mitigates: email flooding
Serve /.well-known/security.txt
This is a simple text file on your server. It provides, at minimum, an e-mail address for security researchers to contact if they find a security problem with your site. I've received two such reports for my site that were quite helpful.
Contact: example@example.com
Preferred-Languages: en
Canonical: https://example.com/.well-known/security.txt
Mitigates: anything a security researcher might notice