Skip to Main Content
July 09, 2020

8 Keys to Writing Safer Code

Written by Mike Spitzer

All too often, security in code is an afterthought. There's a reason that bug bounties are so prevalent; as codebases get larger, testing gets harder. Add in the time constraints of a "move fast and break things" mentality and it's no wonder so many security issues arise. The basics might be there, encrypted connections, hashed passwords, etc., but little thought is given to the possible vulnerabilities of the code behind it. In that vein, I have eight recommendations for writing secure code. I'll be focusing on Python in this post, but these rules can generally apply to any language.

1. Testing, testing!

Ensure that tests exist for everything you code. To assist with this, there are some rules you can follow for code layout. More functions are better than fewer. Break your code up into the smallest chunks you can and make each of those a function. If the function comments are something like, "Do X, then Y," see if you can break it in two. Ideally, write the test before the function. You know what it should do, so you should be able to write the test first. As you move forward with releases and fixes, every time you find a bug, write a test for it, that will help with regression later. And yes, this is general coding advice, but smaller functions are easier to read and follow and help to limit the number of security issues that could appear. Smaller functions can also save you time in tracking down the source of a security problem.

2. Make code readable

Along the lines of being easier to read, it's common to want to save space and typing, but don't do it to the detriment of readability.

Example 1:

Consider the following code:

for i in range(5):
    n = i * 2
    m = 5
    print(n + m)

Made shorter:

for i in range(5): n=i*2; m=5; print(n+m)

Example 2:

Or, consider a list comprehension:

new_list = []
for item in old_list:
    if item < 10:
        new_list.append(item)

Made shorter:

new_list = [item for item in old_list if item < 10]

These statements do the same thing, but I find the first one of each far more readable and the difference is only a few keystrokes. These are simple examples but trying to condense your code can make things harder to see, allowing more bugs or security issues through.

3. Don't rely on assertions to protect code.

Assertions are great for testing; they ensure that values are expected. However, in Python, assertions are only checked when "__debug__" is set to "True," which is the default. Often, in production, you want to optimize your runtime, and turning off "__debug__" is one way to do that. Many (most?) Python frameworks disable "__debug__" when placed in production mode. This means, for example, that the assertion you were using to prevent unauthorized access to certain functions will never be called. It's unlikely you would catch this in testing, but it would be an embarrassing vulnerability to release.

4. Understand how the modules you use work

It's great to import a module, copy some code you found on StackOverflow, and have a working feature. However, actually reading the documents and understanding how the modules functions can save you time and effort down the line. For example, I see a lot of code that uses yaml.load to parse yaml data. It's there, it works, it'll pass any standard tests you throw at it. Did you know that it also has the ability to call any Python function? Easily craftable yaml payloads can do anything the user running the affected code can do. A great example is a vulnerability uncovered in Ansible vault (https://talosintelligence.com/vulnerability_reports/TALOS-2017-0305):

$ ansible-vault view pwned
Vault password:
!!python/object/apply:os.system ["echo 'Hi from Talos!'; id; uname -a"]

$ ipython
In [1]: from ansible_vault import Vault
In [2]: v = Vault('password')
In [3]: v.load(open('pwned').read())

Hi from Talos!
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),112(lpadmin),113(sambashare)
Linux fuzz0 4.4.0-31-generic #50-Ubuntu SMP Wed Jul 13 00:07:12 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

Importing yaml.safe_load will prevent those calls from being loaded, preventing this scenario.

5. Don't try to reinvent the wheel

Don't try to re-invent the wheel unless you really, really need a different kind of wheel (hint: you don't need a different kind of wheel). Especially when the technology is heavily used, your programming language likely has some kind of framework already built for it. For SQL, Python users often use SQLAlchemy. Rather than worry about input sanitization and all the possible errors, you can import a module and use that to abstract all your queries. Larger modules like this aren't full-proof, but are used enough that they have a lot of eyes on them. Major security vulnerabilities are usually rare and fixed quickly. Consider using https://pyup.io to track your dependencies and issues that might arise from using these modules. Along those lines, ensure that your module imports are clean. Don't import anything you don't explicitly need, as some modules can change default functionality.

6. Don't lock versions!

For internal projects, this isn't as big of a deal, but for anything the public will touch, do not lock module versions. It can be very tempting to say, "This works as is, I don't want to break something, so I'll freeze." Freezing prevents updates that can break your system and updates that can protect your system. It's non-discriminatory. Instead, rely on a robust testing regimen that can catch those issues before release. It's fine to freeze something temporarily because of some functionality changed, but it needs to be a priority to update your code to meet any newer requirements.

7. Don't commit keys or passwords

Sometimes, the code isn't the problem. Being able to save state at any time is what's great about version control systems. That said, ensure that passwords and keys are kept inside separate configs and not in your repos. It can sometimes happen unintentionally, and most companies simply remove the file, commit again, and move on, ignoring that the info is still in the file's history. Other companies will go an extra step and attempt to ensure that the commit is removed completely. With Git, this requires the cooperation of everyone who has ever pulled that commit. Don't assume it's gone; revoke the key/password immediately, call it a learning experience, and move on.

8. Make use of existing tools

There are some tools that can help with auditing your code. For Python, I use both Bandit (https://github.com/PyCQA/bandit) and PyT (https://github.com/python-security/pyt). I recommend both, as I've found they each find things that the other doesn't. Additionally, Detect Secrets (https://github.com/Yelp/detect-secrets) can be used to find secrets in your code, like keys and passwords. One advantage of Detect Secrets is that it can baseline existing code, allowing you to move forward if violations in an existing codebase are found. That lets you prevent new secrets from leaking while still allowing updates.

This is far from a complete list, but it should get you started. It can be tempting to just complete a project and push it out as quickly as possible, but some advanced planning and thought with regard to security can save you time, money, and possibly legal or PR problems later. A few extra keystrokes or an extra day or two of planning is not going to sink a project. Also, if Python isn't your primary language, some quick research should point out any equivalents you can use. As a rule, if your language of choice doesn't have these types of tools, you really shouldn't be using it in production. Let it mature some more before jumping to latest hotness in your enterprise environment.

Complete our 5 question reader survey