| # OpenCV ONNX importer out-of-bounds read in getMatFromTensor |
|
|
| This repository contains a proof-of-concept malicious ONNX model that crashes OpenCV's |
| `cv2.dnn.readNetFromONNX` with an out-of-bounds read at model-load time (SIGSEGV). |
|
|
| It is a security PoC for a huntr Model File Format report. The model is intentionally malformed |
| and the repo is gated. |
|
|
| ## Affected |
|
|
| - `opencv-python` / OpenCV `cv2.dnn`, confirmed on **4.13.0** (latest at time of writing), on Linux. |
| - The vulnerable code is unchanged on `master`. |
| - Entry point: `cv2.dnn.readNetFromONNX(path)` (also reachable via `readNetFromONNX` from a buffer). |
|
|
| ## Root cause |
|
|
| `modules/dnn/src/onnx/onnx_graph_simplifier.cpp`, function `getMatFromTensor`. It reads a tensor's |
| dims into a `sizes` vector and copies the tensor bytes into a `cv::Mat` without validating that |
| `raw_data` actually holds that many elements: |
|
|
| ```cpp |
| std::vector<int> sizes; // = tensor dims, attacker controlled |
| for (int i = 0; i < tensor_proto.dims_size(); i++) |
| sizes.push_back(tensor_proto.dims(i)); |
| ... |
| // FLOAT path |
| Mat(sizes, CV_32FC1, (void*)tensor_proto.raw_data().c_str()).copyTo(blob); |
| ``` |
|
|
| The source `Mat` is a header over the (tiny) `raw_data` buffer but carries the declared element |
| count. `copyTo` allocates the destination for the declared count and `memcpy`s that many bytes from |
| the source, reading far past the end of `raw_data`. There is no size check, and no |
| `CV_Assert(blob.isContinuous())` guard (the TensorFlow and Darknet importers have such guards; this |
| ONNX path does not, and it has no length assertion at all). |
|
|
| ## Proof of concept |
|
|
| `poc.onnx` is an 80-byte model whose single FLOAT initializer declares `dims = [10000000]` but |
| carries only 16 bytes of `raw_data`. Loading it makes `getMatFromTensor` allocate a 40 MB |
| destination and copy ~40 MB from the 16-byte source, reading off the end of the heap buffer. |
|
|
| ``` |
| pip install opencv-python onnx numpy |
| python make_poc.py # writes poc.onnx |
| python verify.py # loads poc.onnx in a child process; reports the crash |
| ``` |
|
|
| Observed on opencv-python 4.13.0 (Linux): the child process terminates with SIGSEGV (exit -11) |
| inside `readNetFromONNX`, before any inference. On Windows it terminates with an access violation |
| (`0xC0000005`). A non-vulnerable build would load the model or raise an ordinary exception. |
|
|
| ## Impact |
|
|
| Any application that loads an untrusted or attacker-supplied `.onnx` through OpenCV is exposed. |
| Loading third-party ONNX models is a normal, documented use of this API. An 80-byte file reliably |
| crashes the process via an out-of-bounds read in native code (a denial of service), and because the |
| over-read bytes are copied into the model's weight blob, the read can also disclose adjacent process |
| heap memory (information leak). |
|
|
| ## Fix |
|
|
| In `getMatFromTensor`, validate the tensor's declared element count against the actual size of the |
| data source before constructing the source `Mat` / calling `copyTo`, for every dtype path (FLOAT, |
| DOUBLE, FP16, INT32, INT64, raw_data). For example require |
| `raw_data.size() == elementCount * elemSize` (and the analogous check for the typed `*_data` |
| fields), and reject the model otherwise. |
| |
| ## Relation to the Caffe importer issue |
| |
| This is a distinct vulnerability from the OpenCV Caffe importer out-of-bounds issue in |
| `caffe_importer.cpp` `blobFromProto`: a different importer, a different function |
| (`getMatFromTensor` in `onnx_graph_simplifier.cpp`), and a different model format (ONNX). It also |
| manifests differently, faulting on Linux via the source over-read rather than only on Windows. |
| |