search menu icon-carat-right cmu-wordmark

API Security through Contract-Driven Programming

Alex Vesey

According to MITRE, the most common form of API (application programming interface) misuse occurs when the caller does not honor its end of a contract. In the context of this article, a "contract" refers to a formal, precise agreement that outlines the expected behaviors, inputs, outputs, and side effects that an API guarantees to any caller, ensuring that both the API and its clients adhere to specified constraints and usages. This concept is crucial in preventing misuse by clearly defining the boundaries and requirements for both parties involved in the interaction. This blog post explores contract programming and specifically how that applies to the building, maintenance, and security of APIs.

API misuse often occurs due to not knowing the state of the system behind an API, which may lead to incorrect ordering of calls and end in an error state that is a vulnerability. This misuse can also happen when an implementation of an API does not meet the specification. For example, a consumer may be expecting a certain output per the specification but receive something different. Finally, misuse of an API can happen in an object-oriented programming (OOP) context with specific subclass implementations. These implementations may not provide the same functionality that is mandated by the super class or interface. In the design and implementation of software systems there exists a theory of contracts that can help to solve some of these issues.

An API is, in a general sense, a contract between a provider of a software and the consumer of that software about what the system will do. This idea of contract programming or Design by Contract was coined by Bertrand Meyer in 1986. In this paradigm, a software engineer defines formally the specification for each function or method that the system exposes ((in the context of this paper, the terms “function” and “method” are used interchangeably). This specification includes noting pre-conditions, post-conditions, and invariants. While generally a good design practice for enhancing the verifiability of systems, this contract programming construct also enables API security.

Pre- and Postconditions of a Contract

We define a functions contract as the set of pre- and post- conditions and the invariants of the function that must hold.

Preconditions are the set of criteria required before a function can be executed. These are things that the service or API provider expect to be true before a function is called. An example of this, in the context of an API, is that a precondition for accessing a protected endpoint be that the caller provides a valid authentication token. Another example is a function that requires a valid (i.e., not null) pointer be passed to it. In either of these cases, if the precondition is not met (i.e., the token is invalid, or the pointer is null), then the contract is broken.

Postconditions are the state or set of criteria that must be true after a function is executed. Postconditions for an API may be the return of some specified data and an HTTP 200 status code. A caller or consumer that uses an API function whose preconditions are not met is not entitled to the postconditions. The system that is furnishing the API is expected to provide the post conditions. Finally, invariants are the data or state that cannot be changed by function execution regardless of the operation or transformation applied by the function.

Therefore, to honor the contract means to answer the three questions of a Hoare triple:

  1. What does the contract expect?
  2. What does the contract guarantee?
  3. What does the contract maintain?

Defining an API Interface

As an interface, an API typically is defined in data definition language (DDL), interface description language (IDL), or just plain text. Consequently, an interface’s implementation may not be true to the specification. Formal methods provide a means of verifying that an implementation refines a specification. Ensuring an implementation meets all expectations of an interface is also closely tied to the Liskov Substitution Principle. In discussing both refinement of a specification and the Liskov substitution principle we can generalize the following constraints for a function:

  1. Preconditions cannot be strengthened (i.e., an implementation may not accept a narrower range of input than the specification dictates). For example, an implementation of a pop() method on a Stack cannot add a precondition that the stack must have a minimum size of five elements before allowing a call to pop().
  2. Postconditions cannot be weakened (i.e., an implementation may not return a larger range of output than the specification dictates). For example, after calling push(element) on a Stack the stack must reflect the addition of exactly one new element, but not more.
  3. Invariants cannot be weakened (i.e., an implementation may not alter the state of invariants listed in the specification). For example, the size of a stack must never be negative, regardless of the number of pop() or push() operations performed.

In addition to errors on the consumer side of an API, errors can also be caused by not fully implementing the interface of the API or doing so incorrectly. For example, the Open Worldwide Application Security Project (OWASP) foundation publishes a list of the top 10 API security risks. For 2023 the top risk was Broken Object Level Authorization (BOLA). BOLA is an example of an implementation not honoring a contract precondition, such as a request to a given API function or endpoint must contain an authorization token that is valid for the particular object being requested.

Who Should Check the Pre-and Postconditions?

This question depends on the style and architecture of the codebase that is implementing an API. In many cases the provider of the API will require strong preconditions and will not even attempt to work if they are not met. This constraint puts the burden on the client to ensure that everything is valid and in the proper state before calling an API. On the other hand, the techniques of defensive programming suggest that it is potentially better to handle unforeseen circumstances more gracefully. Meyer suggests when designing by contract that handling one case well and requiring strong preconditions is a best practice that has proved successful.

Programming Language Tools for Defining API Contracts

How contracts are defined in a particular language varies. In Java the use of Javadoc comments to document the parameters, return value, exceptions, and the functions purpose is a common (though less formal) way of documenting a contract. There are also a variety of tools that offer varying levels of formality for defining contracts that can help to enable verification of API usage. Some notable examples are:

  • Eiffel
  • Java via Java Modeling Language (JML)
  • Kotlin (natively)
  • Rust via the contracts crate
  • Ada 2012 (natively)
  • API Blueprint
  • OpenAPI

Given the prevalence of HTTP-based REST APIs, OpenAPI is a relevant tool and format for specifying endpoints, input and output for each operation, authentication methods, and other information. The use of the OpenAPI specification to define an API aligns well with the design-by-contract paradigm of specifying the preconditions, such as the domain of inputs and a description of the endpoint. OpenAPI also allows for specifying the return from an endpoint including the return code, a description of what that return code means, the schema of any returned data, and examples of the data.

Specifically, in the realm of API security, OpenAPI also allows for specification of the authentication and authorization requirements for each endpoint. In the documentation, OpenAPI refers to this as a security scheme. In the 3.0 version, this security scheme includes HTTP authentication, API Keys, OAuth2, and OpenID Connect.

Potentially a place that OpenAPI falls short is in the ability to specify invariants of a function. For instance, in a REST API, GET requests should be idempotent. There is, however, no way to document outside of a text description what an endpoint may or may not change in terms of state.

While Open API and the other listed tools all offer a machine readable or parseable format, as previously mentioned, even a text description of a functions contract can help. The advantage of a machine-readable contract, however, is the ability to generate test cases for the contract.

There are several open-source tools, such as RESTler and Dredd, that will consume an OpenAPI spec and automatically generate and execute test cases against an implementation. Similarly with Java and the Java Modeling Language (JML), there are applications that can transform the Javadoc comments into runtime assertions. An example of this approach is the JML compiler that adds in assertions to the Java bytecode.

Benefits to API Testing

As we have explored, there are many tools for supporting contract programming. However, these tools come with a cost. In particular, developers must be trained on their use; the tools must be integrated into a product’s DevSecOps pipeline, and they are yet another dependency that must be maintained and updated. In addition to the consumer benefits of providing a contract, what benefits can the API developers get from using these tools? I contend that the biggest advantage to developers operating under the contract programming paradigm is the ability to test the interface without testing the implementation.

Josh Bloch, CMU professor and formerly of Google and Sun Microsystems wrote, “Code the use-cases against your API before you implement it.” A product with a well-defined contract enables the team to test out an API specification and write example client code that uses the functions or endpoints very early on in the development cycle. This approach eliminates any time spent implementing a specific function and then finding out the function is awkward or hard to use form the client perspective.

This concept also extends to integration testing of different software modules. For large, complex systems it can be hard to assemble all the users to perform live testing of each component. Similarly, some systems can prove hard to simulate a test environment for. Perhaps the target system is highly expensive to operate on (such as quantum computers at ~ $1.60 per second) or the system is not even built yet. In both cases having a contract that accurately represents a software module or library can aid the integration testing done by both producers and consumers of the software.

Increasing API Usability Increases Security

While APIs can be used by both humans and other applications, they are ultimately designed and implemented by humans. Ignoring the usability or developer experience of an API can lead to security concerns whereas increasing API usability can bolster security. For example, a study by Sascha Fahl et. al found that in 13,500 popular free Android apps, eight percent had misused the APIs for the Secure Sockets Layer (SSL) or its successor, the Transport Layer Security (TLS), and were thus vulnerable to man-in-the-middle and other attacks. A follow-on study of Apple iOS apps found that 9.7 percent were vulnerable with causes including significant difficulties using security APIs correctly. The authors of the study recommend numerous changes that would increase the usability and security of the APIs.

Brad Myers contends that API security is a function of bad code written by programmers who are human. Easier-to-use APIs therefore help security by making good code easier to write and bad code harder. To support this approach, contract driven programming can be a means to ease the burden of relying on documentation outside of the source code because it has been shown that many software developers prefer to use source code over official documentation.

Every API does not provide source code. However, even for those that are fully open, centralizing the API rules and expectations within a contract can help streamline the developer experience. This concept of a code-driven approach to learning meshes well with the fact that most contract programming mechanisms are directly embedded withing the source code that implements the contract. Having a clear, easy-to-find and easy-to-use API contract can prevent unintentional misuse.

Another example of a broken contract that had security implications is Heartbleed. In the implementation of OpenSSL, the heartbeat request message could be exploited to overread the buffer when asking for more data than the payload needed. This exploit was a violation of the contract in the sense that the payload_length field should have been the same as the payload but was not. In retrospect, this error is a classic buffer over-read, but it affected many systems. If a contract has explicitly outlined the precondition that the payload and payload length must be the same, the error may have been more obvious to the implementer. While there are other means to solve this same problem through automated code repair or using languages with more strict compilers, contract driven programming could provide a language agnostic way to avoid similar errors in implementation.

The Future of API Contract Programming

Contract programming in the context of APIs is a powerful concept that can help ensure an API conforms to a specification. APIs by their nature represent a black box where an implementation and the how of the system is opaque to the user. Given the nature of APIs, it is important to inform API users what exactly is needed and what to expect. A standardized approach to representing these contracts helps testers automate and validate APIs. Well-defined contracts can also aid in the developer experience of an API and provide more formal verification of systems that require even more assurance.

Additional Resources

Get updates on our latest work.

Each week, our researchers write about the latest in software engineering, cybersecurity and artificial intelligence. Sign up to get the latest post sent to your inbox the day it's published.

Subscribe Get our RSS feed