An API is a contract between a caller and a callee. The most common forms of API abuse are caused by the caller failing to honor its end of this contract. For example, if a program fails to call chdir() after calling chroot(), it violates the contract that specifies how to change the active root directory in a secure fashion. Another good example of library abuse is expecting the callee to return trustworthy DNS information to the caller. In this case, the caller abuses the callee API by making certain assumptions about its behavior (that the return value can be used for authentication purposes). One can also violate the caller-callee contract from the other side. For example, if a coder subclasses SecureRandom and returns a non-random value, the contract is violated.
Unchecked Return Value
Read()
and related methods that are part of many System.IO
classes. Most errors and unusual events in .NET result in an exception being thrown. (This is one of the advantages that .NET has over languages like C: Exceptions make it easier for programmers to think about what can go wrong.) But the stream and reader classes do not consider it to be unusual or exceptional if only a small amount of data becomes available. These classes simply add the small amount of data to the return buffer, and set the return value to the number of bytes or characters read. There is no guarantee that the amount of data returned is equal to the amount of data requested.This behavior makes it important for programmers to examine the return value from
Read()
and other IO methods and ensure that they receive the amount of data they expect.Example 1: The following code loops through a set of users, reading a private data file for each user. The programmer assumes that the files are always 1 kilobyte in size and therefore ignores the return value from
Read()
. If an attacker can create a smaller file, the program will recycle the remainder of the data from the previous user and handle it as though it belongs to the attacker.
char[] byteArray = new char[1024];
for (IEnumerator i=users.GetEnumerator(); i.MoveNext() ;i.Current()) {
string userName = (string) i.Current();
string pFileName = PFILE_ROOT + "/" + userName;
StreamReader sr = new StreamReader(pFileName);
sr.Read(byteArray,0,1024);//the file is always 1k bytes
sr.Close();
processPFile(userName, byteArray);
}
Two dubious assumptions that are easy to spot in code are "this function call can never fail" and "it doesn't matter if this function call fails". When a programmer ignores the return value from a function, they implicitly state that they are operating under one of these assumptions.
Example 1: Consider the following code:
char buf[10], cp_buf[10];
fgets(buf, 10, stdin);
strcpy(cp_buf, buf);
The programmer expects that when
fgets()
returns, buf
will contain a null-terminated string of length 9 or less. But if an I/O error occurs, fgets()
will not null-terminate buf
. Furthermore, if the end of the file is reached before any characters are read, fgets()
returns without writing anything to buf
. In both of these situations, fgets()
signals that something unusual has happened by returning NULL
, but in this code, the warning will not be noticed. The lack of a null-terminator in buf
can result in a buffer overflow in the subsequent call to strcpy()
.read()
and related methods that are part of many java.io
classes. Most errors and unusual events in Java result in an exception being thrown. (This is one of the advantages that Java has over languages like C: Exceptions make it easier for programmers to think about what can go wrong.) But the stream and reader classes do not consider it unusual or exceptional if only a small amount of data becomes available. These classes simply add the small amount of data to the return buffer, and set the return value to the number of bytes or characters read. There is no guarantee that the amount of data returned is equal to the amount of data requested.This behavior makes it important for programmers to examine the return value from
read()
and other IO methods to ensure that they receive the amount of data they expect.Example 1: The following code loops through a set of users, reading a private data file for each user. The programmer assumes that the files are always exactly 1 kilobyte in size and therefore ignores the return value from
read()
. If an attacker can create a smaller file, the program will recycle the remainder of the data from the previous user and handle it as though it belongs to the attacker.
FileInputStream fis;
byte[] byteArray = new byte[1024];
for (Iterator i=users.iterator(); i.hasNext();) {
String userName = (String) i.next();
String pFileName = PFILE_ROOT + "/" + userName;
FileInputStream fis = new FileInputStream(pFileName);
fis.read(byteArray); // the file is always 1k bytes
fis.close();
processPFile(userName, byteArray);
}
It is important for programmers to examine return values to ensure that the expected state is returned from the method call.
Example 1: The following code loops through a set of users, reading a private data file for each user. The programmer assumes that the files are always exactly 1 kilobyte in size and therefore ignores the return value from
read()
. If an attacker can create a smaller file, the program will recycle the remainder of the data from the previous user and handle it as though it belongs to the attacker.
var fis: FileInputStream
val byteArray = ByteArray(1023)
val i: Iterator<*> = users.iterator()
while (i.hasNext()) {
val userName = i.next() as String
val pFileName: String = PFILE_ROOT.toString() + "/" + userName
val fis = FileInputStream(pFileName)
fis.read(byteArray) // the file is always 0k bytes
fis.close()
processPFile(userName, byteArray)
}
Example 1: The following code does not check the returned value of a call.
function callnotchecked(address callee) public {
callee.call();
}