The following content will likely see significant revision, though can be used as a reference for security best practices to follow when developing code for Mozilla.
Introduction
- Provide developers with information on specific security issues
- Cover common coding mistakes and
- How they affect a product
- How to avoid making them
- How to mitigate them
- Everything is oriented toward C/C++
Introduction: Gaining Control
- Specifics about the underlying architecture, using x86 as an example
- 6 basic registers (EAX, EBX, ECX, EDX, EDI, ESI)
- 2 stack-related registers (ESP, EBP)
- Mark top and bottom of current stack frame
- Status register (EFLAGS)
- Contains various state information
- Instruction pointer (EIP)
- Points to register being executed; can’t be modified directly
Introduction: Gaining Control (2)
- EIP is modified using call or jump instructions
- Attacks usually rely on obtaining control over the EIP
- Otherwise the attacker can try to control memory pointed to by an existing function pointer
- A vulnerability is required to modify the EIP or sensitive memory
- Saved return addr or function pointer get altered
Introduction: Gaining Control (3)
- Common issues used to gain control
- Buffer overflows
- Format string bugs
- Integer overflows/underflows
Writing Secure Code: Input Validation
Input Validation
- Most vulnerabilities are a result of un-validated input
- Always perform input validation
- Could save you without knowing it
- Examples:
- If it doesn’t have to be negative, store it in an unsigned int
- If the input doesn’t have to be > 512, cut it off there
- If the input should only be [a-zA-Z0-9], enforce it
Cross Site Scripting (XSS)
- XSS is a type of code injection attack
- Typically occurs in web applications
- Injection of arbitrary data into an HTML document from another site
- Victim’s browser executes those HTML instructions
- Could be used to steal user credentials
- Think: webmail, online auction, CMS, online banking...
XSS: Example
The following snippet of JavaScript:
document.write("Welcome to " + document.location);
... is exploitable (in some browsers) with a simple request such as:
https://www.victim.com?something=<SCRIPT>alert('Oops')</SCRIPT>
XSS: Prevention
- Escape all dynamic input that will be sent back to the user
- HTML encoding
- & → &
- < → <
- > → >
- " → "
- ' → '
- URL encoding
- % encoding
- Java/VBscript escaping
- Depends on the context; in a single-quoted string, escaping ' would suffice
SQL Injection
- Occurs when un-trusted input is mixed with a SQL string
- SQL is a language used to interact with databases
- Code injection attack that is similar to XSS but targeted at SQL rather than HTML and JavaScript
- If input is mixed with SQL, it could itself become an SQL instruction and be used to:
- Query data from the database (passwords)
- Insert value into the database (a user account)
- Change application logic based on results returned by the database
SQL Injection: Example
snprintf(str, sizeof(str),
"SELECT * FROM account WHERE name ='%s'", name);
sqlite3_exec(db, str, NULL, NULL, NULL);
SQL Injection: Prevention
- Use parameterized queries
- Insert a marker for every piece of dynamic content so data does not get mixed with SQL instructions
- Example:
sqlite3_stmt *stmt;
char *str = "SELECT * FROM account where name='?'";
sqlite3_prepare_v2(db, str, strlen(str), &stmt, NULL);
sqlite3_bind_text(stmt, 1, name, strlen(name), SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_finalize(p_stmt);
Writing Secure Code: Arithmetic Issues
Integer Overflows/Underflows
- Overflows occur when an arithmetic operation attempts to create a numeric value that is larger than can be represented within the available storage space
- MAX + 1 will be 0 and 0 – 1 will be MAX!
Bits | Maximum value that can be represented | Data type | |
8 | 28-1 | 255 | char |
16 | 216-1 | 65535 | short |
32 | 232-1 | 4294967295 | int |
64 | 264-1 | 18446744073709551615 | long long |
Integer Overflows/Underflows
- Example of an integer overflow
int main() {
unsigned int foo = 0xffffffff;
printf(“foo: 0x%08x\r\n”, foo);
foo++;
printf(“foo: 0x%08x\r\n”, foo);
}
Integer Overflows/Underflows
- Example of an integer underflow
int main() {
unsigned int foo = 0;
printf(“foo: 0x%08x\r\n”, foo);
foo--;
printf(“foo: 0x%08x\r\n”, foo);
}
Integer Overflows/Underflows
- Real-life example (bug 303213)
JSBool js_str_escape(JSContext *cx, JSObject *obj, unsigned int argc, jsval *argv, jsval *rval){
...
newchars = (jschar *) JS_malloc(cx, (newlength + 1) * sizeof(jschar));
...
for (i = 0, ni = 0; i < length; i++) {
if ((ch = chars[i]) < 128 && IS_OK(ch, mask)) {
newchars[ni++] = ch;
...
}
...
}
...
}
Integer Overflows/Underflows: Prevention
- Difficult to fix: You need to check every arithmetic operation with user input
- Arithmetic libraries like SafeInt can help
Signedness Issues
Bits | Data type | Range |
8 | signed char | -128 - +127 |
unsigned char | 0 - +255 | |
16 | signed short | -32768 - +32767 |
unsigned short | 0 - +65535 | |
32 | signed int | -2147483648 - +2147483647 |
unsigned int | 0 - +4294967295 | |
64 | signed long long | -9223372036854775808 - +9223372036854775807 |
unsigned long long | 0 - +18446744073709551615 |
int vuln_funct(int size) {
char buf[1024];
if (size > sizeof(buf) - 1) return -1;
}
Signedness Issues:
- Don’t mix signed and unsigned integers
- Use unsigned integers for sizes, offsets, and indexes
Casting and Truncation
- Example:
void vuln_funct() {
u_int32_t size1 = 0xffffffff;
u_int16_t size2 = size1;
void *ptr = malloc(size2);
if (!ptr) exit(EXIT_FAILURE);
memcpy(ptr, user_data, size1);
}
Casting Issues: Sign Extension
- Example:
int main() {
int32_t new_size = 0;
int8_t size = 0xFF;
new_size = size;
printf("0x%08x\r\n", new_size);
}
Casting Issues: Sign Extension Prevention
- Be careful with signed integers
- Use unsigned integers for sizes, offsets, and indexes
Denial of Service: Divide by Zero
- Example:
int main() {
int a, b;
if (argc != 3) return 1;
a = atoi(argv[1]);
b = atoi(argv[2]);
return a/b;
}
Denial of Service: Divide INT_MIN by -1
- Example:
int main(int argc, char **argv) {
int a, b;
if (argc != 3) return 1;
a = atoi(argv[1]);
b = atoi(argv[2]);
return b ? a/b : 0;
}
Writing Secure Code: Memory Management
String Handling
- C-style strings are byte arrays that end with a \0 byte
- Some string handling functions won’t perform any kind of length checking, so don’t use them
- Ensure your string is always \0 terminated!
Buffer Bounds Validations (BBV)
Thou shalt check the array bounds of all strings (indeed, all arrays), for surely where thou typest "foo" someone someday shall type "supercalifragilisticexpialidocious".
BBV: Stack Overflow
- Example:
void foo(char *bar) {
char c[12];
strcpy(c, bar);
}
int main(int argc, char **argv) {
foo(argv[1]);
}
BBV: Stack Overflow
Before the stack overflow
BBV: Stack Overflow
After the stack overflow
BBV: Heap Overflow
- Dynamic memory
- malloc()
- calloc()
- HeapAlloc()
- mmap()
- Not on the stack segment!
- Most memory allocators use a linked list or binary tree
BBV: Off-by-One
The array index starts at 0 not at 1
- char array[1024];
- array[0] = first element!
- array[1023] = last element!
- array[1024] = 1 element outside the array!
BBV: Array Indexing Issues
- Always ensure that the index is within the bounds of the array
- Never use signed integers as index
- buf[strlen(buf) - 1] = '\0';
- strlen() could return 0; resulting in a negative index!
BBV: Array Indexing Issues
- Always ensure that the index is within the bounds of the array
- Never use signed integers as index
- buf[strlen(buf) - 1] = '\0';
- strlen() could return 0; resulting in a negative index!
BBV: Prevention
- Check the bounds of your arrays
- Use a safe and well-designed API
- When using integers as array indexes, use caution
Format String Bugs
- Example:
int main(int argc, char *argv[]) {
if (argc > 1) printf(argv[1]);
}
Format String Bugs: Prevention
- Easy to fix: Always use a format string specifier:
int main(int argc, char *argv[]) {
if (argc > 1) printf("%s", argv[1]);
}
Double Free
- Example:
void* ptr = malloc(1024);
if (error) {
free(ptr);
}
free(ptr);
Double Free: Prevention
- Set a pointer to NULL when it’s freed
- Valgrind or malloc debugging can help detect those bugs
Use After Free
- Accessing data after a
free()
ordelete
can lead to undefined behavior - Some debug tools might be able catch some cases
Un-initialized Data
- Example:
int main() {
char *uninitialized_ptr;
printf("0x%08x\r\n", uninitialized_ptr);
return 0;
}
$ ./test
0x8fe0103
Un-initialized Data: Prevention
Initialize your variables!
Memory Leaks
- Example:
void *p;
size_t new_size;
p = realloc(p, new_size);
if (p == NULL) {
/* handle error */
}
Memory Leaks: Prevention
Tools like Valgrind can help detect memory leaks
Writing Secure Code: Object Management
Reference Counting Issues
- Real-life example (bug 440230)
void AddRef() {
++mRefCnt;
NS_LOG_ADDREF(this, mRefCnt, "nsCSSValue::Array", sizeof(*this));
}
void Release() {
--mRefCnt;
NS_LOG_RELEASE(this, mRefCnt, "nsCSSValue::Array");
if (mRefCnt == 0)
delete this;
}
Reference Counting Issues: Prevention
- Use the largest data type available on your platform for your reference counter
- Use a hard limit
Constructor/Destructor Issues
- If a constructor fails the destructor never gets called
- This can lead to memory leaks
Constructor/Destructor Issues
- Example
class foo {
private:
char *ptr;
public:
foo() {}
~foo() {
if (ptr)
free(ptr);
}
};
Constructor/Destructor Issues: Prevention
Initialize the data members of an object in the constructor
Writing Secure Code: Miscellaneous
File I/O
- A lot can go wrong because a lot can be done with file input and output
- Filenames
- Permissions
- File handles and descriptors
File I/O: Filename
- Divided in directories, subdirectories, and the file itself
- ‘/’ is separator; in Windows ‘\’ would work too
int openfile(char *file) {
HANDLE fh;
if (strstr(file, “\”))
return -1;
fh = CreateFileA(file, ...);
WriteFile(fh, data, sizeofdata, NULL, NULL);
} - Could be a normal file, directory, device, or link
- Directory traversal (../../../../)
File I/O: File Permissions
- Should be set correctly
- Be sure not to make world-writable files
- Sensitive files shouldn’t be world readable
File I/O: File Descriptors and Handles
- Could be a race if instances of fh are shared between threads
- Fh inheritence: default in Unix, needs to be set in Windows
int main(int argc, char **argv, char **envp) {
int fd = open("/etc/shadow", O_RDWR);
setreuid(getuid(), getuid());
excve("/bin/sh", argv, envp);
} - suid root applications
File I/O: File Descriptors and Handles
- Potential overflows when using select
- fd_set struct, static length, holds a bitmask of fds
- Manipulated with FD_SET, FD_ISSET, FD_CLR and FD_ZERO macros
- fd_set’s size depends on the operating system
- If the OS allows opening more fds, then fd_set can hold
- Could overflow fd_set
File I/O: File Descriptors and Handles
Good solution: dynamically allocate fd_set structs
int main(void) {
int i, fd;
fd_set fdset;
for( i = 0; i < 2000; i++) {
fd = open("/DEV/NULL", O_RDWR);
}
FD_SET(fd, &fdset);
}
File I/O: Race Conditions
- Operating on files can often lead to race conditions since the file system is shared with other processes
- You check the state of a file at one point in time and immediately after the state might have changed
- Most file name operations suffer from these race conditions, but not when performed on file descriptors
File I/O: Race Conditions
- Consider the following example
int main(int argc, char **argv) {
char *file = argv[1];
int fd; struct stat statbuf;
stat(file, &statbuf);
if (S_ISLINK(statbuf.st_mode)) {
bailout(“symbolic link”);
}
else if (statbuf.st_uid != getuid) {
bailout(“you don’t own the file”);
}
fd = open(file, O_RDWR);
write(fd, argv[2], strlen(argv[2]));
}
File I/O: Race Conditions
- Previous example contains a race condition
- The file may change between the call top stat() and open()
- This opens the possibility of writing arbitrary content to any file
Race Conditions
- Occur when two separate execution flows share a resource and its access is not synchronized properly
- Race condition types include
- File (previously covered)
- Thread (two threads share a resource but don’t lock it)
- Signal
Race Conditions
- Example
char *ptr;
void sighandler() {
if (ptr)
free(ptr);
_exit(0);
}
int main() {
signal(SIGINT, sighandler);
ptr = malloc(1000);
if (!ptr)
exit(0);
... do stuff ...
free(ptr);
ptr = NULL;
}
Race Conditions
- Previous example is vulnerable to a signal race condition
- What would happen if the application received a signal in the middle of free(ptr)?
- It would lead to a double free
Race Conditions: Prevention
- Be very careful when working with threads, the file system, or signals
- Track down shared resources and lock them accordingly
- For signal handlers
- Never use non-atomic operations
- longjmp() is a sign of badness
- Even exit() could cause problems, but _exit() is okay
Deadlocks and Locking Issues
- Locks are used when
- Dealing with threads
- Acquiring more than one lock to perform an action
- If a second thread acquires the same locks but in a different order, it causes denial of service since both threads will be waiting forever
Deadlocks and Locking Issues
- Example
func_a() {
lock(lockA);
lock(lockB);
... do something ...
unlock(lockB);
unlock(lockA);
}
func_b() {
lock(lockB);
lock(lockA);
... do something ...
unlock(lockA);
unlock(lockB);
}
Writing Secure Code: Good Coding Practices
Banned API List
- Badly designed APIs can often lead to vulnerabilities
- It’s too easy to use the API inappropriately
- For example, consider the libc string handling APIs
- Strcpy() performs no bounds checking
- Strncpy() doesn’t always 0-terminate
- Strncat() length parameter is very confusing
Banned API List
- Examples of incorrect strncat usage
- Buffer overflow
strncat(buffer, string, sizeof(buffer));
- Off-by-one
strncat(buffer, string, sizeof(buffer) – strlen(string));
- Buffer overflow
- Correct usage
strncat(buffer, string, sizeof(buffer) – strlen(string) – 1));
Banned API List: Recommendations
- Create wrappers or replacements for standard functions with a bad design
Libc function name | Reason to ban it |
strcpy, strcat, gets, sprintf, vsprintf | No bounds checking. |
strncpy | Does not guarantee \0 termination. |
strncat | Confusing size argument; can write 1 byte beyond the size. |
snprintf, vsnprintf | Return value differs on various platforms. |
alloca | Never use this. Calling alloca() with a user-controlled size == game over. Use malloc() instead. |
Using the Mozilla API
- Use C++ strings so C strings are possible
- Mozilla C safe string handling APIs
- Similar to libc str*cpy and str*cat
- Some are potentially dangerous
Function name | Comments |
PL_strcpy, PL_strcat | These functions don't verify whether the destination buffer is large enough. |
PL_strncpy | This function doesn't null terminate. |
PL_strncat | This function is confusing because the size argument is the maximum number of bytes being appended, not the size of the buffer. This can lead to security bugs. |
PL_strncpyz | This function copies a string and guarantees null termination. |
PL_strcatn | The size argument for this function is the size of the buffer and it guarantees zero termination. |
Using the Mozilla API
- Mozilla C++ style strings are less prone to buffer overflows and 0-termination issues
- Every string derived from the abstract base class nsAString
- Common read-only methods
- Length()
- isEmpty()
- Equals()
- Common methods for modifying the string
- Assign()
- Append()
- Insert()
- Truncate()
Checking Return Values
- Often causes problems
- Return value not handled
- Certain cases not handled or interpreted incorrectly
- Double meaning
- malloc() can return a pointer or NULL, but NULL by itself is a valid address
Checking Return Values
int main() {
int fds[2];
pipe(fds);
write(fds[0], "data", 4);
}
- The pipe() return value is not checked
- If pipe() fails, fds is not initialized
- Write to un-initialized file descriptor
Checking Return Values
- Check all return values—no matter how unlikely the API failure
- For example:
- close() can fail and leak file descriptor
- setuid() can fail and privileges don’t get dropped
- snprintf() can fail and result in return value -1
- tmp = realloc(tmp, size) — realloc could fail and leak tmp
Writing Secure Code: Exception Handling
Double Freeing Pointers
- Double frees can occur in exception handling
- They free data in try block and then free it again in the catch block
- This is a common issue
Double Freeing Pointers
char *ptr1 = NULL, *ptr2 = NULL;
try {
ptr1 = new char[1024];
do_something();
delete ptr1;
do_something_else();
ptr2 = new char[-1] // OOM
} catch (...) {
delete ptr1;
delete ptr2;
}
- New will throw an exception — ptr1 is already freed in the try block then freed again in the catch block
Freeing Un-initialized Data
- Free data in the catch block
- Assumption is that it’s initialized in the try block
- If the try block throws before the variable is initialized, the catch block will operate on un-intialized data
Freeing Un-initialized Data
- Example:
int main(){
char *ptr;
try {
ptr = new char[-1]; // OOM
} catch(...) {
delete ptr;
}
}
Freeing Un-initialized Data: Prevention
- Be careful when freeing data in catch blocks
- Make sure the try block can’t throw before data is initialized
- Initialize variables when they’re declared; for example, set pointers to NULL
Memory Leaks
- Usually occur when memory is allocated in a try block
- After the allocation, something throws in the try block before memory is freed in the try block
- The catch block does not foresee this and doesn't free the memory
Freeing Un-initialized Data
- Example:
int main(){
char *p;
try {
p = new char [1000];
... code that might throw an exception ...
delete p;
} catch (...) {
...
}
}
Memory Leaks: Prevention
- Any acquired resource in a try block should be freed in a catch block (if the try block throws before the resource is freed)
- Might be helpful to initialize variables
- NULL for pointers
- -1 for file descriptors