Security Checklist for Software Developers
Security should be foreseen as part of the system from the very beginning, not added as a layer at the end. The latter solution produces insecure code (tricky patches instead of neat solutions), may limit functionality and will cost much more (in time/money).
Here are some hints and tips for various phases of the software development lifecycle. This list certainly doesn't contain all pieces of advice that software engineers should follow. It just includes the most important ones - those which are definitely worth keeping in mind when designing and developing almost any kind of software with any programming language!
- Think of the security implications of what your code does;
- Read and follow guidelines for your programming language and software type;
- Reuse trusted code (libraries, modules etc.);
- Write good-quality, readable and maintainable code (bad code won't ever be secure).
- Modularity: Divide the program into semi-independent parts (small, well-defined interfaces to each module/function);
- Isolation: Each part should work correctly even if others fail (return wrong results, send requests with invalid arguments);
- Defense in depth: Build multiple layers of defense instead of trusting just one protection mechanism. For example: validate user input data at entry point, and check again all values that are passed to sensitive parts of the code (like file handling etc.);
- Simplicity: Complex solutions are much more likely to be insecure.
- Make security-sensitive parts of your code small;
- Least privilege principle: Don't require more privileges than you need. For example: run your code as the least privileged user necessary (don't run it as root, nor with SUID flag). Make sure that the account on which you will run your code has only the file access and execute privileges that your code really needs. Don't connect to a database with admin privileges from your software;
- Choose safe defaults: For example, a random password that users are likely to change rather than a standard default passwords that many won't bother to change;
- Deny by default: For example, when validating user input accept only characters that you expect rather than trying to block known "bad" characters;
- Limit resource consumption, to limit the likelihood or impact of a Denial of Service attack;
- Fail securely: For example, if there is a runtime error when checking user's access rights, assume she has none;
- In distributed or Web applications don't trust the client: Don't expect it to validate user input, perform security checks or authenticate users - it all has to be done (again) on the server side; remember that HTTP response header fields (cookies, user-agent, referrer etc.) and HTTP query string values (from hidden fields or explicit links) may be forged/manipulated;
- Cryptography: Use trusted, public algorithms, protocols and products. Do not invent your own cryptographic algorithms or protocols, nor implement existing ones - reuse trusted code.
- Don't trust input data: Data coming from potentially malicious users is the single most common reason of security-related incidents (buffer overflow, SQL injection, Cross Site Scripting (XSS), code inside data etc.). Input data includes command-line arguments, configuration files (if accessible by not-trusted users), environment variables, cookies and POST/GET arguments etc;
- Validate all input data: Consider all input dangerous until proven valid, deny by default if not sure, validate at different levels, for example at input data entry point and before really using that data;
- Don't make any assumptions about the environment: make sure your code doesn't break with modified/malicious PATH, CLASSPATH and others environment variables, current directory, @INC Perl variable, umask, signals, open file descriptors etc;
- Beware of race condition: Can your code run parallel? what if someone executes two instances of your program at the same time, or changes environment in the middle of its execution?
- Deal with errors and exceptions: Don't assume that everything will work (especially file operations, system and network calls), catch exceptions, check result codes; don't display internal error messages, failing SQL query, stack trace etc;
- Fail gracefully: If there is an unexpected error that you can't recover from, then log details, alert the administrator, clean the system (delete temporary files, clear memory) and inform the user;
- Protect passwords and secret information: don't hard-code passwords (it's hard to change them and easy to disclose), use external files instead (possibly encrypted) or already existing credentials (like certificates), or simply ask the user for the password;
- Be careful when handling files: If you want to create it, report an error if it already exists; when you create it, set file permissions; if you open a file to read data, don't ask for write access; check if the file you open is not a link with the lstat() function (before and after opening the file); use absolute pathnames (for both commands and files); be extra careful when the filename (or part of it) comes from a user;
- Temporary files: Avoid them whenever possible. Pipes are a safer and more efficient way of communicating information between processes. If you really need remporary files, don't fall for the symbolic link attack (someone guesses the name of your temporary file, and creates a link from it to another file e.g. /bin/bash, that your program overwrites). Temporary files must have unique names that are hard to guess! (use tmpfile() for C/C++, mktemp shell command etc.);
- Be careful with shell calls, eval functions etc.: Such functions evaluate the string argument as code and interpret it, or run it on the shell. If an attacker managed to inject malicious input to that argument, you're executing his code.
After the Implementation
- Review your code and let others review it;
- When a (security) bug is found, search for similar ones;
- Use tools specific to your programming language: Bounds checkers, memory testers, bug finders etc;
- Turn on compiler / interpreter warnings and read them (perl -w, gcc -Wall);
- Disable debugging information (strip command, javac -g:none, etc.).