diff --git a/fuzzing/CMakeLists.txt b/fuzzing/CMakeLists.txt index b930c7d6..aee2c972 100644 --- a/fuzzing/CMakeLists.txt +++ b/fuzzing/CMakeLists.txt @@ -21,3 +21,6 @@ target_link_libraries(FuzzStunClient turnclient ${LIB_FUZZING_ENGINE}) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/input/FuzzStunClient_seed_corpus.zip DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + +add_executable(FuzzOAuthToken FuzzOAuthToken.c) +target_link_libraries(FuzzOAuthToken turnclient ${LIB_FUZZING_ENGINE}) diff --git a/fuzzing/FuzzOAuthToken.c b/fuzzing/FuzzOAuthToken.c new file mode 100644 index 00000000..3a6594ef --- /dev/null +++ b/fuzzing/FuzzOAuthToken.c @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * + * https://opensource.org/license/bsd-3-clause + * + * Fuzzing target for OAuth token decoding (issue #1838). + * + * Exercises decode_oauth_token() against arbitrary input, with focus on: + * - Stack buffer overflow when nonce_length field > OAUTH_MAX_NONCE_SIZE (256) + * - encoded_field_size underflow in unsigned subtraction + * - OpenSSL GCM IV-length handling with out-of-range nonce lengths + * + * The fuzz input is treated as the raw bytes of an encoded_oauth_token. A + * fixed oauth_key with a known algorithm is used so the fuzzer exercises the + * parsing and bounds-checking paths rather than key derivation. + */ + +#include +#include + +#include "ns_turn_msg.h" +#include "ns_turn_msg_defs.h" + +/* 16-byte all-zero key for A128GCM (128-bit key size). */ +static const uint8_t kFuzzAsRsKey[16] = {0}; + +/* Minimum meaningful token: 2 (nonce_len) + nonce + 4 (key_len) + 8 + * (timestamp) + OAUTH_GCM_TAG_SIZE (16) + 1 (at least one ciphertext byte) + * With nonce_len = 0 that is 2 + 0 + 4 + 8 + 16 + 1 = 31. */ +#define kMinInputLength 31 +#define kMaxInputLength MAX_ENCODED_OAUTH_TOKEN_SIZE + +extern int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + if (Size < kMinInputLength || Size > kMaxInputLength) { + return 0; + } + + oauth_key key = {0}; + key.as_rs_alg = A128GCM; + memcpy(key.as_rs_key, kFuzzAsRsKey, sizeof(kFuzzAsRsKey)); + key.as_rs_key_size = sizeof(kFuzzAsRsKey); + /* auth_key_size must be non-zero for the integrity path; use the same key. */ + memcpy(key.auth_key, kFuzzAsRsKey, sizeof(kFuzzAsRsKey)); + key.auth_key_size = sizeof(kFuzzAsRsKey); + + encoded_oauth_token etoken = {0}; + etoken.size = Size; + memcpy(etoken.token, Data, Size); + + oauth_token dtoken = {0}; + + /* + * The return value is intentionally ignored: we are looking for crashes + * (buffer overflows, use-after-free, etc.), not for decryption success. + * + * Invariant: if decoding claims success, the nonce_length stored in + * dtoken must fit within the nonce buffer. + */ + bool ok = decode_oauth_token((const uint8_t *)"fuzz-server", &etoken, &key, &dtoken); + + if (ok) { + /* nonce_length must be within the fixed buffer declared in the struct. */ + if (dtoken.enc_block.nonce_length > OAUTH_MAX_NONCE_SIZE) { + __builtin_trap(); + } + } + + return 0; +} diff --git a/src/client/ns_turn_msg.c b/src/client/ns_turn_msg.c index c1611460..8f0916b4 100644 --- a/src/client/ns_turn_msg.c +++ b/src/client/ns_turn_msg.c @@ -2349,6 +2349,13 @@ static bool decode_oauth_token_gcm(const uint8_t *server_name, const encoded_oau const unsigned char *csnl = snl; const uint16_t nonce_len = nswap16(*((const uint16_t *)csnl)); + + if (nonce_len > OAUTH_MAX_NONCE_SIZE) { + OAUTH_ERROR("%s: nonce length too large: %u > %u\n", __FUNCTION__, (unsigned)nonce_len, + (unsigned)OAUTH_MAX_NONCE_SIZE); + return false; + } + dtoken->enc_block.nonce_length = nonce_len; const size_t min_encoded_field_size = 2 + 4 + 8 + nonce_len + 2 + OAUTH_GCM_TAG_SIZE + 1;