Skip to main content

Chainlink CBOR Audit

Smart Contract Security Assessment

Jan 28, 2022

Chainlink

SUMMARY


ABSTRACT

Dedaub was commissioned to perform a security audit on Chainlink’s CBOR Solidity library, including it’s dependencies and some of it’s clients . Most of the audit effort was expended in examining CBOR.sol and it’s dependency, Buffer.sol. We also examined Chainlink.sol and ChainlinkClient.sol, which make heavy use of the CBOR library.

The commit hash/version of the files are the following:

  • CBOR.sol: 6b059e40f27be25e991b2c8dc819b28dfceb5bef
  • Buffer.sol:
    • v0.0.13, installed as an npm dependency of CBOR
    • Latest github version: bcff2f93378a03cf9b3376237faf0a56a7896830
  • Chainlink.sol/ChainlinkClient.sol: aa5a452c2a3761fcb24fccc7165b42aae015023e

SETTING & CAVEATS

Chainlink’s original approach to getting off-chain data on to the blockchain was the “request” model: a highly flexible system, where users were able to post arbitrary query parameter sets and the response would be posted on-chain after it the request had been processed by the Chainlink network.

While the “request” model has not seen much use in the past years due to the introduction of the more user-friendly feed model, the ever-increasing demand for cross-chain functionality has revitalized the interest in this more flexible model.

CBOR is a binary data representation format that was designed with compactness and ease of encoding/decoding in mind. The latter feature makes it really attractive as a format for encoding these off-chain request parameters. It can be thought of as the binary counterpart of JSON, as it supports a similar set of value types (integers, arrays, strings, mappings etc).

Chainlink has implemented a Solidity library to encode data into CBOR format, to provide a user friendly interface to any client that wants to handle CBOR data or use their “request” model. A core dependency is the ensdomain/Buffer library, which is a library for mutable memory buffers written in Solidity.

While the CBOR format is relatively simple, the low-level, direct memory manipulation that the Buffer library performs, the extensive use of inline assembly and the important role the CBOR library has in it’s “request” model, make this a crucial piece of code that needs to be carefully implemented.

Note that due to the fact that the Buffer & CBOR contracts are elementary building blocks for larger contracts, this audit treated them as standalone general-purpose contracts. That is, we report vulnerabilities that could affect an arbitrary contract that uses Buffer/CBOR, even if Chainlink’s use of the contracts itself is not vulnerable.


VULNERABILITIES & FUNCTIONAL ISSUES

This section details issues that affect the functionality of the contract. Dedaub generally categorizes issues according to the following severities, but may also take other considerations into account such as impact or difficulty in exploitation:

Category
Description
CRITICAL
Can be profitably exploited by any knowledgeable third-party attacker to drain a portion of the system’s or users’ funds OR the contract does not function as intended and severe loss of funds may result.
HIGH
Third-party attackers or faulty functionality may block the system or cause the system or users to lose funds. Important system invariants can be violated.
MEDIUM
Examples:
  • User or system funds can be lost when third-party systems misbehave.
  • DoS, under specific conditions.
  • Part of the functionality becomes unusable due to a programming error.
LOW
Examples:
  • Breaking important system invariants but without apparent consequences.
  • Buggy functionality for trusted users where a workaround exists.
  • Security issues which may manifest when the system evolves.

Issue resolution includes “dismissed”, by the client, or “resolved”, per the auditors.


CRITICAL SEVERITY

[No critical severity issues]


HIGH SEVERITY

H1

Outdated Buffer.sol version

H1HIGH

Outdated Buffer.sol version
resolved

The CBOR project package.json lists @ensdomains/buffer v0.0.13 as a dependency, which is outdated. The latest version of the Buffer.sol library fixes two important issues in Buffer::writeUint8:

  • The capacity was not properly computed in case of a resize
  • The buffer’s length was updated when a byte was being appended at the end of the buffer, but not when written at any larger offset.

In fact, we discovered these bugs while auditing the CBOR contract, and only later realized that they were already fixed by very recent commits in the Buffer repository.

Note that these issues do not affect the current CBOR code, but we still recommend updating to the latest version, to avoid future versions of CBOR becoming vulnerable.

H2

Buffer library can lead to serious memory corruption if misused

H2HIGH

Buffer library can lead to serious memory corruption if misused
resolved

The Buffer library performs some really low-level manipulations of the Solidity memory. Most critically, it manually updates the Solidity Free Memory Pointer (FMP) whenever new space needs to be allocated, incrementing it by the capacity of the newly allocated buffer. However, this update is done entirely inside an inline assembly block, which means that no overflow checks are done.

As a consequence, if a contract calls Buffer::init(buf, capacity), in a context where capacity can be controlled by the adversary, the FMP will be updated to FMP+capacity+32. This value might overflow the uint256 range, with two major consequences:

  • the FMP will now point to allocated memory, causing unpredictable behavior
  • The buffer’s content will overlap with previously allocated memory, possibly allowing the adversary to manipulate sensitive memory content via the buffer.

Note that the same vulnerability can be exploited via the write* methods, even if Buffer::init itself is not controllable by the adversary, if the offset parameter is user-controllable. Consider, for instance, the following code:

function write(buffer memory buf, uint off, bytes memory data, uint len) internal pure returns(buffer memory) {

if (off + len > buf.capacity) {
resize(buf, max(buf.capacity, len + off) * 2);
}

It could be that the expression (off + len) * 2 does not overflow, but when the resize happens, FMP + (off + len) * 2 does overflow!

We strongly recommend checking for free memory pointer overflows in Buffer::init.



MEDIUM SEVERITY

M1

Uninitialized memory

M1MEDIUM

Uninitialized memory
resolved

In Buffer.sol, the write functions allow writing at an offset that is greater than the length of the current buffer. This is handled by simply updating the buffer’s length, without initializing the part of the buffer up to that offset, as shown in the code below (from Buffer::write).

if gt(add(len, off), buflen) {
mstore(bufptr, add(len, off))
}

Note that the memory contents in solidity are not guaranteed to be zeroed (since previous code might have used the part after the FMP as temporary scratchpad). As a consequence, the data in the range buf[old_length:offset] are never initialized, and may potentially contain garbage data.

It is recommended that this range in the buffer be zero initialized.



LOW SEVERITY

L1

The CBOR library does not guarantee that correct CBOR is generated

L1LOW

The CBOR library does not guarantee that correct CBOR is generated
partially resolved

The CBOR library uses a low-level API that puts the burden of generating valid CBOR to the user. There are numerous ways of generating invalid CBOR via the library:

  • Starting but not ending arrays/maps
  • Invalid nesting of arrays/maps
  • Maps with odd number of elements
  • etc

Depending on the use-case, it might be preferable to use a higher-level API that enforces correct CBOR output (or a way to test that the output is valid).



OTHER/ ADVISORY ISSUES

This section details issues that are not thought to directly affect the functionality of the project, but we recommend addressing them.

A1

No support for fixed-length arrays/maps

A1ADVISORY

No support for fixed-length arrays/maps
partially resolved

Although the CBOR specification allows for fixed-length arrays and maps, these are not supported by the CBOR library. Extending CBOR::startArray/startMap to support them would be trivial, and likely useful for several applications.

In particular, the array in Chainlink::addStringArray has known length. Using a fixed length array would make the CBOR one byte shorter, and also save the self.buf.endSequence(); call.

A2

Optimization: Common Subexpression Elimination

A2ADVISORY

Optimization: Common Subexpression Elimination
resolved

In Buffer::write(buffer buf, uint off, bytes* data, uint len), the subexpression off + len appears multiple times throughout the body of both write functions.

Calculating it once and storing it in a local variable could help save some gas and also help with readability.

A3

Optimization: Redundant max operation

A3ADVISORY

Optimization: Redundant max operation
resolved

In Buffer::write(buffer, uint, bytes, uint), there is a redundant max operation:

function write(..) {
if (off + len > buf.capacity) {
// Dedaub: Max operation is redundant; will always be (len + off)
resize(buf, max(buf.capacity, len + off) * 2);
}
...
}

Replacing the operation with just (len + off), or a local variable with that value (as per the A1 suggestion) would help save some gas. The same issue also appears in Buffer::writeUint8.

A4

Misleading Solidity pragma range

A4ADVISORY

Misleading Solidity pragma range
resolved

In CBOR.sol, the pragma solidity directive ranges from version 0.4.19 up to (excluding) 0.9.0. However, Buffer.sol has pragma solidity ^0.8.4; , which makes it impossible to compile CBOR with any Solidity version lower than 0.8.4.

It is recommended that the CBOR directive be updated to have 0.8.4 as a lower bound.

A5

Compiler known issues

A5ADVISORY

Compiler known issues
info

The contracts were compiled with the Solidity compiler v0.8.7 which, at the time of writing, have some known bugs. We inspected the bugs listed for version 0.8.7 and concluded that the subject code is unaffected.



DISCLAIMER

The audited contracts have been analyzed using automated techniques and extensive human inspection in accordance with state-of-the-art practices as of the date of this report. The audit makes no statements or warranties on the security of the code. On its own, it cannot be considered a sufficient assessment of the correctness of the contract. While we have conducted an analysis to the best of our ability, it is our recommendation for high-value contracts to commission several independent audits, a public bug bounty program, as well as continuous security auditing and monitoring through Dedaub Security Suite.


ABOUT DEDAUB

Dedaub offers significant security expertise combined with cutting-edge program analysis technology to secure some of the most prominent protocols in DeFi. The founders, as well as many of Dedaub's auditors, have a strong academic research background together with a real-world hacker mentality to secure code. Protocol blockchain developers hire us for our foundational analysis tools and deep expertise in program analysis, reverse engineering, DeFi exploits, cryptography and financial mathematics.