9. Secure Coding Practices#
9.1. Getting Started#
Code doesn’t happen in a vacuum. In the real world, you’ll be developing software that interacts with other applications, packages, and, importantly, human users. These interactions have inherent vulnerabilities, particularly if the code handles sensitive data. Think about, for example, a payment app that collects customer credit card information. If the app leaves that information exposed to unknown third parties—some of whom could be bad actors, such as hackers—the customers could quickly become victims of theft or even fraud. Understandably, then, users anticipate and desire security when interacting with software. Most code is not impenetrable, but you can follow secure coding practices throughout the development process to reduce the ways bad actors could misuse the program.
Image Source: https://vpnoverview.com/internet-safety/business/what-is-secure-coding/
9.2. So what even are secure coding practices?#
Secure coding practices are protocols that you can use to minimize security vulnerabilities while building software. Think of all the different ways you can develop a program while still maintaining the same purpose. Secure coding practices guide you to develop the program in the most defensive manner possible. Let’s take a look at the some of the secure coding practices outlined by OWASP, an open-source nonprofit project dedicated to improving the security of software. These practices are a popular international standard.
9.3. Security By Design#
This first one might seem obvious, or even like—uh oh—circular logic: “Secure your code by securing its design! Make good choices by choosing good choices! Boil water by boiling water!” But as noted before, prioritizing security throughout the entirety of the design process is really important when minimizing security vulnerabilities. This can look like a lot of things: thinking about edge cases in design or santizing inputs are normal seeming practices that are apart of secure coding! Though security is usually not the only priority for developers, because it may seem like short-term priorities like speed, efficiency, and scalability are sometimes at odds with security. However, time and time again, we see that “security by design” pays off in the long run.
9.4. Input Validation and Output Encoding#
We’re combining these two practices, which basically say the same thing: know your data. On a trusted system (this means server side), identify everything you can about the program’s input data and sources through a centralized process, and when in doubt, validate it! Classify all input sources as trusted or untrusted, and validate untrusted sources. Likewise, validate the range, length, and character sets (ex: UTF-8) of input data, and add additional security protocols and discrete checks for the suspicious stuff, a.k.a. unidentifiable data.
9.5. Real Life Example#
In 2018, Daniel Lemire found a way to validate UTF-8 strings using 5 lines of C++ code, shown below. If you wanna read more about his research, check it out here: https://lemire.me/blog/2020/10/20/ridiculously-fast-unicode-utf-8-validation/
simd8 classify(simd8 input, simd8 previous_input) {
auto prev1 = input.prev<1>(previous_input);
auto byte_1_high = prev1.shift_right <4>().lookup_16(table1);
auto byte_1_low = (prev1 & 0x0F).lookup_16(table2);
auto byte_2_high = input.shift_right <4>().lookup_16(table3);
return (byte_1_high & byte_1_low & byte_2_high);
}
9.6. Passwords and Authentication#
Ya know, *********** Passwords and authentication. We’ve all been there. You know, when you’re making an online account for something, and you have to create a password with at least eight characters, including a number, and a capital letter, and a special character, and the secret to the universe (the answer is 42). And then you have to re-enter the password to prove that you have dextrous thumbs and a better memory than a goldfish, so you have to erase and retype your password because you don’t know where you messed up because all you can see are asterisks or those weird dots… And this process feels like it takes forever, but you still can’t help but think: remember the good old days of passwords when ‘password’ would suffice? Or better yet, 1234?
Ah, yes. Well. Security practices improve. Life goes on. C’est la vie.
When in doubt, use a standardized and tested authentication protocol—like a password—for users to access a web page unless that page is meant to be fully accessible to the public, and develop that protocol on a trusted (read: server side) system.
For passwords, set adequate standards of complexity and length (hint: 1234 isn’t good enough). Obscure the password for users as they type it, and, yes, have users confirm their password. Use proper hashing and cryptography—strong, one-way salted hashes—when storing password information server side, and make sure authentication information is securely stored. Another important thing to note is to not let users reuse the same password particularly if they are doing a password reset, and if they reset their password, alert the user of that reset. Also, set a standard threshold for the number of attempts a user can enter an incorrect password before disabling the account. Don’t let users change their password for at least one day after they create their account. And if you send new users temporary passwords, make sure the password has a short expiration time, and force users to change the password during their first login. If a user must perform a critical operation in their account, have them re-authenticate themselves by password. Lastly, passwords are not enough for highly sensitive or transactional accounts. Use multi-factor authentication!
Here’s an example of a code snippet that salts a password in Python:
import hashlib
import os
password = "mysecretpassword"
# Generate a random salt
salt = os.urandom(32)
# Create a SHA-256 hash object
hash_object = hashlib.sha256()
# Add the salt to the password and hash it
hash_object.update(salt + password.encode())
# Get the hex digest of the hash
hash_password = hash_object.hexdigest()
print(hash_password)
c25f226d489c2a1252ee80de505720a1294701cfc59e2db5d6838e999290a11a
Code source: https://pagorun.medium.com/password-encryption-in-python-securing-your-data-9e0045e039e1
9.7. Session Management#
Any time a user logs into their account, they have begun a “session.” Developers must manage their software’s session controls, or it can get a little chaotic. Here are a few guidelines:
Manage and identify these sessions on your own server or framework, and if a user tries to begin a session outside of that framework, treat their session as invalid. Try to establish at least one new session with each login, and maintain HTTPS connection (as opposed to HTTP) throughout the session. Do not allow the same user to log in concurrently. And when a user logs out, the session should completely end.
9.8. Access Control#
Two words: default deny. When in doubt, assume that users are tyring to get information outside of their scope and limit their access when needed. Only allow authenticated users to access any non-public information. And if the application cannot access its secure configuration information, do not allow any users to access the application. Finally, ensure the application’s access controls comply with all business rules and flows while still protecting against potential attacks.
9.9. Cryptographic Practices#
Cryptography, you know, the funky symbols you see when a hacker gets into the mainframe in any tv show/movie since the 80s. Essentially, use cryptography to protect any sensitive information from users, and implement all cryptographic practice on a trusted system. Use cryptographically-based randomized number generators to generate random numbers and strings. And manage your cryptographic keys under a secure, standardized process and policies.
This is an example of encryption in Python:
Source: https://cryptography.io/en/latest/
from cryptography.fernet import Fernet
#put this somewhere safe! (Not very secure for the security chapter, huh...)
key = Fernet.generate_key()
f = Fernet(key)
token = f.encrypt(b"A really secret message. Not for prying eyes.")
token
b'...'
f.decrypt(token)
b'A really secret message. Not for prying eyes.'
b'A really secret message. Not for prying eyes.'
9.10. Error Handling and Logging#
Errors. They’re inevitable in life, especially in software development. As you might have heard before, the best way to deal with software errors is to handle and log them. But error handling and logging can introduce new vulnerabilities in addition to any bugs that might already exist in the code. So we must—you guessed it—handle and log errors securely. How do we securely handle and log errors? Well, I’m so glad you asked!
Firstly, when in doubt, log it. Log all errors, exceptions, failures, and potential tampering events. And write and manage these through cryptographic hashing. And when you do deal with errors, free all allocated memory from that error.
Here’s an example of a log file for a web server:
Image Source: https://www.researchgate.net/figure/A-sample-of-Web-Server-Log-File_fig1_220773991
9.11. Data protection#
Similarly to default deny, use least privilege! Keep users from seeing any data or information unessential to their usage or functioning. Store all sensitive data and information with cryptographic practices, and remove all sensitive data and information when you no longer need it. And remember, access controls are your friend!
9.12. Communication security#
Transmit all sensitive information via encryption, especially Transport Layer Security, or TLS. TLS acts as a protective layer for transported information and should protect information as such. Use TLS for all sensitive content and external system communications. If TLS fails, ensure the backup connection is also secure.
9.13. Memory management#
Ah, memories! Well, memory in this case.
Follow these tips and tricks to handle memory with care! Keep buffering controlled. Don’t use functions that are known to be vulnerable. Be sure to close all resources yourself—garbage collection is easy to fall back on, but don’t rely on it for everything. Lastly, properly free all allocated memory, and overwrite any sensitive information that allocated memory may contain at all exit points in a given function.
9.14. Conclusion#
We’ve covered some secure coding practices to help you protect your software. While this is a start, there is still so much more to the wonderful world of secure coding practices! This was based off of OWASP’s Secure Coding Checklist, which can give you more detailed information on secure coding practices, check it out here: https://owasp.org/www-project-developer-guide/draft/verification/guides/web_security_testing_guide/
Happy coding!