Podcast: Play in new window | Download (54.0MB) | Embed
Subscribe: Apple Podcasts | Spotify | Email | RSS | More
“Defensive programming is a form of defensive design intended to ensure the continuing function of a piece of software under unforeseen circumstances. Defensive programming practices are often used where high availability, safety, or security is needed.” ~ wikipedia
Most of us have heard about the software bug in the Therac-25 radiation therapy machine that caused the death of five people. A bug in the onboard guidance system of the ESA’s Araine 5 rocket caused it to self destruct 40 seconds after takeoff. Flaws added to gas pipeline control software by the CIA to trick Soviet spies lead to the largest non nuclear explosion in history.
Defensive programming is a way of thinking and writing your code to be on the look out for the unexpected. Your code will get blind sided at some point in it’s lifetime. Use the information here to help mitigate that when it happens. Build your software so that it is capable of handling the unexpected challenges it will face when you release it into the wild.
Understanding Defensive Coding
Defensive coding is a way of designing code to guarantee it’s continued functioning no matter the circumstance. Code should work no matter the inputs. The happy path is the expected correct inputs from the user. The unhappy paths are the expected incorrect inputs from the user. Defensive coding handles even the unexpected inputs from the user. These techniques are used when you must have high availability, safety, or strong security. The app needs to be running constantly or near constantly with little to no down time for updates or failures. Poor error handling or failure can create security risks. Not handling unexpected events creates it’s own attack surface.
Defensive programming focuses on three areas: overall quality, predictability, and comprehensibility. Improve the overall quality of the code by reducing the bugs and problems in it. The application should function regardless of user input, whether it is expected or not. Source code must be understandable by other developers who will be maintaining it long after you have moved on.
Defensive coding is about asking the right questions about the app you are building. It’s only using the absolute necessary code and nothing more. Design the architecture for the big picture of what the app will be doing. This means looking beyond the individual feature being built. It looks at how that feature will interact with the rest of the codebase. It also involves designing the code to be extensible. This allows future developers to make additions without a lot of overhead. This will also allow for quicker bug fixes and changes when initially developing the app.
Offensive vs Defensive Coding
Offensive coding is a subset of defensive coding that focuses on failing fast. The idea is not to handle unexpected events but to quickly identify them. If your code can’t handle the exception then it shouldn’t handle it. The code should throw an error or pass the exception up to a higher level that can handle it. Use offensive coding when code cannot handle the exception that is thrown. It could be that you just don’t know how to handle the error. Ideally, the code doesn’t swallow the exception but that is not always the case. Many times the only thing returned is the stack trace. Basically, custom exceptions do not exist.
Many times offensive coding comes about due to time constraints or overly pressured deadlines. You don’t have time to build in exception handling. However, you need to identify or at least acknowledge when an unexpected event occurs. This can also come from overuse of the KISS principle. You are so focussed on each line of code doing exactly what it’s supposed to do you don’t think about what happens if it doesn’t. It also happens when you are so focussed on the effects of the particular segment of code that you don’t think about potential side effects. This is worst with legacy systems where no-one is still around from when it was originally written.
Defensive coding is about developing the feature that the user needs in a manner that is maintainable and doesn’t interfere with existing functionality. Here the idea is to be prepared for unexpected events. You want to handle them gracefully. This means more than logging an error. It could be retrying a particular function call or having a special path for unexpected events or inputs. Defensive coding is about designing a sustainable architecture from the beginning. This occurs before any code is ever written. A lot of times this is a mental game that you do while relaxing before starting on a project or app. It involves asking the right questions and understanding the subject material enough to ask intelligent questions.
Defensive coding takes into account the big picture and all that an app has to offer. It even leaves doors open for unplanned or even impossible at the time features. To code defensively you’ll need to know all the constraints with which you are working. This includes an idea of where things can go wrong in your application. You’ll also need to know know the details about what functionality is needed from the application. Exception handling is a key component of defensive coding. Exception handling is more than just logging and swallowing an error. In addition to logging descriptive errors need to be sent to the user to allow them to alter the data they are sending to match the expected information.
Techniques of Defensive Coding
Reuse existing source code as often as possible. It is already tested with known functionality and known quirks. Look at when and why it was written. Well tested legacy code may have been written before some problems were known or even existed. Back in the 1990’s and even early 2000’s the internet was a much more trusted place. It was the code written back then that lead to the OWASP Top 10. Make sure it doesn’t have any legacy problems. These are problems in the older design that may not work with the new requirements. The older design was not tested with the new requirements in mind. The legacy code may not have been written defensively. It also may have been written and tested under circumstances that may not apply.
Use canonicalization libraries to avoid issues caused from non-canonical input. Canonicalization is the process of converting user input into a standard data format. It’s also known as normalization or standardization.
Canonicalization is used to count or compare data structures. It also improves algorithm efficiency. This is especially important for filenames and paths in security. Your app may have access to a particular folder or file. But using features built to help traverse the file storage via the command line, “../”, can allow malicious input access to other areas.
Canonicalization will reduce the attack surface for directory traversal attacks. String comparison can become tricky when adding accents to letters. It’s possible to have two Unicode characters, one for the letter and on for the accent. It’s also possible to have just one unicode character representing the letter with the accent.
Standardization allows for easier comparisons that don’t have to take into account all possible ways of creating the same character. The Canonical XML Specifications define an XML form for standardization of XML documents. It removes redundant namespaces and white spaces in tags. Also it transforms relative URIs into absolute URIs.
Have a low tolerance against potential bugs. Follow the smelly code to find where it’s not very fresh. Assume that if it looks problematic it will be problematic. Fix bugs as soon as they are found. Refactor areas that are prone to be buggy. All bugs are potential security holes. You cannot know all types of security exploits. New ones are coming out all the time. Protect your code against even the ones that you don’t know about.
Check the validity of your inputs using a design by contract or assertive programming model. Design by contract makes sure that data is sanitized. It does so by using preconditions, postconditions, and invariants. The code is able to document assumptions. It means that arguments passed into functions are validated before the function is executed. Also within the function a state check is performed before the value is returned from the function.
When calling a function you want to assert that you are not referencing something that is invalid. Check that array lengths are valid before referencing the array. Do null checks on values that shouldn’t be null when passed into the function. This is especially important if the function takes an non-nullable but the value you are passing is nullable. The function may not have a default value.
Create a small validation library of function for asserting or checking values before passing them. Make sure to include logging so you can trace a path if something isn’t validated. This will help reduce the tedium of defensive programming. Don’t trust libraries that you did not write, check everything.
Do’s and Don’t’s of Defensive Coding
Do not trust user input.
Always assume that you’ll get something unexpected from the user. You know all the functionality and how it’s supposed to work. Testers know what it’s supposed to do and the expected invalid inputs. Users don’t know any of this information so they will have their own ideas about what your app should do.
Assume that everything unexpected is malicious. Most of it will be poor user training or incompetence. However, by assuming it’s all malicious you won’t miss something hiding out as an incompetent user. You’re not coding for the 99% of the time it’s safe but the 1% that it is malicious.
Use whitelists not blacklists. Filter by what is allowed not by what is not allowed. Doing this will ensure that you know what you are working with. For example, when validating upload mime types don’t look for invalid types, instead only allow valid ones.
Do use database abstraction.
Number one on the OWASP Top 10 has been Injection since the beginning of the list. This is such an issue that it’s remained at the top of the list of vulnerabilities since it’s inception in 2003. Injection happens when code is put in instead of the usual input. This can lead to all sorts of problems.
Parameterize your inputs going into the database. Most ORMs will do this for you, that’s what they are built to do. This will stringify any user input that might be malicious code.
Do not reinvent the wheel.
Use a framework where available. They are there to reduce the redundancy that causes complacency in development. They have been built by teams and tested. Use them where you don’t need to create something new.
Don’t create new functions or features when you could use existing ones. The existing code is already battle tested in the wild. The only time to create something new is if what exists doesn’t fit your needs. Just remember to validate your inputs and outputs when using existing code.
Do create tests before your code gets to QA.
Writing tests will help you stick to best practices. This could be using SOLID principles or specific design patterns. Your tests will ensure you are following these practices.
Use them to test your code and your objects. You’ll see how many objects your “small” code changes affect when mocking them up. This will give you insight into integration testing and what all could be affected.
Tips for Effective Defensive Coding
Write your code as clean as possible.
This will improve comprehension of your code. Make the functionality and responsibility of your code clear. This lowers the risk of inept users misunderstanding or misusing the code.
Keep your methods specific and focused. Name them according to what they do. Spend time naming variables and methods.
Clean code is easier to maintain. When you can see what the previous developer was attempting to do it makes fixing issues much easier. Being the only one who can maintain your code means interrupted vacations.
Prefer descriptive exceptions over return codes.
Return readable exceptions that express the part of the app that is not working. This could be enforcing part of an API contract. You want to guide the user of your app or API to a solution, not confuse them.
Error or return codes only slow the process of finding a solution. Don’t send system details in your error response. Log stack traces and other things but send a message back that explains what went wrong. “Invalid input for …” will tell the user what they need to know without exposing your system. Send something like, “You can’t order ‘asdf’ beers because ‘asdf’ isn’t a number.”
Assume you are in hostile territory.
Start with the assumption that your code will constantly be under attack. You will not know when or where the attack on your code will take place. You do know that it will come though.
Malicious users will attempt to exploit any weakness in your application. Defensive coding is about not only preparing for the known attacks but having contingencies in place for the unknown attacks.
Developing a Defensive Mindset
Know the purpose of the code you are writing. Learn enough about the business to know how your app will be used. Find out not only what the app will be doing but how they would do it without the app. Knowing the long process will help you understand how to make it simpler. This will also identify areas where people might try to circumvent the system.
Your app is there to help the business do it’s job. This is not the time to show how intelligent you can be. Remember that debugging is always harder than coding…
If you don’t know why you are writing it, you will write bad code. The code may be flawless and execute exactly as written but not meet the business needs. When this happens the users will look for ways around your code. That can lead to them creating security issues.
Write your code with maintenance in mind. We’ve all been guilty of writing to solve the immediate issue. This could be hard coded values or not using proper design patterns. It happens when you have to get something done hard and fast.
Not building with maintenance in mind will cause you to leave vulnerabilities. It will also encourage future developers who are maintaining it to create vulnerabilities. This gets even worse when it becomes legacy code that someone has to learn.
Be consistent throughout your code base. If you do something one way in one place do it the same in all the places. Inconsistent code is hard to read and understand. You’re not defending against other developers on your team. It can lead to confusion especially as a codebase ages.
When working with multiple developers create coding standards. This will ensure that the code is consistent no matter who wrote it. If you have trouble getting buy-in from fellow developer make it a requirement for accepting a pull request.
Resist the urge to add more features. This may be ones that you foresee or ones that are requested in meetings. First off your foresight isn’t that great, noone’s is. Next, your default should always be “no” to new code changes unless it’s a planning meeting.
Resist adding more code or features until you are sure they are necessary. These extra changes take time away from your main focus. ALso they add increased risk of defects and security holes. To know if a new feature is needed look back at the app’s purpose. Ask if the new change helps achieve that purpose. If so then it is a necessary change.
How to Think Like a Coder (Without Even Trying!)
The final section of the book looks at taking your learning further. It starts with where to go when finished with the book. This includes a list of things you can do to improve your learning. Next it talks about coding as a form of volunteering to help your community. Next it has a few games to keep you thinking like a coder and some deeper concepts to learn more about.
Tricks of the Trade
Paranoia is healthy when they are out to get you.