In 2026, Shielder was hired by OSTIF to perform a security audit of Power Grid Model, an open-source library for steady-state distribution power system analysis, developed under the LF Energy umbrella and maintained by Alliander.
Today, we are publishing the full report in our dedicated repository.
Power Grid Model (PGM) is a high-performance computational backend for electrical distribution grids: given a model of the grid (nodes, lines, transformers, loads, generators…), it performs Power Flow, State Estimation and Short Circuit calculations. It is used across the energy sector for grid planning, expansion, reliability and congestion studies.
Under the hood, PGM is made of three layers:
On top of that, the auxiliary power-grid-model-io library takes care of converting grid data from and to other formats (e.g., Excel/CSV files from other grid tools) using YAML mapping files. Given how often it is used to feed data into PGM, we included it in scope.
See it in action:
| |
As with every audit, we started with threat modeling. With libraries, we have found this to be crucial: the code alone can’t tell you how developers in the “real world” are using it, where the interesting inputs come from. So we studied the documentation, examples and integrations to understand how end-users install, configure and use the project, then refined the model as the code review progressed.
As a result, three main points emerged:
power-grid-model-io converters and their YAML mappings) the most likely entry points for untrusted input.With these attack scenarios in mind, we have performed the audit by combining manual and tool-assisted source code review (with SAST tooling such as semgrep and publicly available C/C++ rules), a fuzzing campaign, and a review of the GitHub workflows for supply-chain attack scenarios.
Since PGM was not on-boarded on OSS-Fuzz yet, we developed a harness using the de facto standard libFuzzer LLVMFuzzerTestOneInput interface, running on the AFL++ engine. As untrusted input reaches the library through a sequence of stages — a document is parsed, written into buffers, turned into a model and finally solved — the team wrote one harness for each of these, so that every stage could be fuzzed at its own speed and depth. The harnesses exercise the following C APIs:
PGM_create_deserializer_from_binary_buffer and PGM_dataset_info_*, to turn the fuzzer input into an in-memory dataset and expose its metadata.PGM_deserializer_parse_to_buffer, to access the deserialized dataset.PGM_create_model, PGM_copy_model and PGM_get_indexer, which build the in-memory model.PGM_calculate, which performs the calculation on the model.PGM_update_model, to apply an update document to an existing model.Since randomly mutated inputs are mostly rejected early by the parser, we then enhanced the fuzzer with a custom mutator that generates structurally valid inputs, so that more test cases could reach the core calculation logic.
The harness was contributed to the project in PR #1617 and will soon be onboarded on OSS-Fuzz, so the project will keep benefiting from it after the audit ended.
While auditing, we have discovered three vulnerabilities (one with High severity, one with Medium, one with Low) that we reported through the GitHub Security portal of the power-grid-model and power-grid-model-io repositories.
Arbitrary Method Invocation via YAML Mapping (CVE-2026-84457)
power-grid-model-io’s Tabular Converter uses YAML mapping files to describe how to convert tabular data (e.g., Excel sheets) into PGM components. Mappings can reference Python functions by their dotted path, to transform values while converting. These paths were resolved by a get_function() helper with a plain import_module() + getattr(), and the resulting function was then invoked with keyword arguments taken from the input data columns.
Although the project’s SECURITY.md mentioned an allow-list to prevent the execution of arbitrary functions, no such check was actually implemented in the Tabular Converter. Therefore, a malicious mapping could reference, for instance, os.popen, map its cmd argument to a data column, and achieve code execution as soon as the mapping is used to load input data.
The severity was set to High: the impact is full code execution in the host process, but the attacker needs to control the YAML mapping (and, to pass arguments, the input data as well).
The maintainers fixed this in power-grid-model-io version 2.0.0 by implementing an allow-list of functions that can be used in the Tabular Converter configuration.
Unbounded JSON Nesting Leads to Denial of Service (CVE-2026-76912)
PGM’s json_deserialize() uses nlohmann’s JSON SAX parser under the hood, with a custom visitor that pushes a new buffer every time it encounters a [ or { token. Since there was no depth check, a deeply nested JSON payload could keep pushing entries until the host process ran out of memory and got killed by the OOM-killer. With an amplification factor of roughly 4000x, a 2MB payload was enough to crash the process on our Ubuntu Server test machine.
The severity was set to Medium, as this only affects availability, and requires the attacker to submit arbitrarily large payloads.
The maintainers fixed this in version 1.13.111 (PR #1467) by introducing a maximum nesting depth for both JSON and msgpack payloads.
Out-Of-Bounds Memory Access in Span Calculation Leads to Denial of Service (CVE-2026-76911)
PGM can run many scenarios in a single batch (e.g., “what if this line goes down?”, “what if that load doubles?”), and each scenario may change a different number of components. To pass them through the C API (PGM_dataset_const_add_buffer()), all the changes are packed one after the other in a single flat buffer, plus an index array called indptr that says where each scenario starts and ends. For example, indptr = {0, 2, 5, 7} means that scenario 0 uses elements 0-1, scenario 1 uses elements 2-4, and scenario 2 uses elements 5-6.
For this to work, every value in indptr must be greater than or equal to the previous one, and none can exceed the buffer size. PGM only checked the first and last values, so arrays such as {0, 5, 2, 7} (going backwards) or {0, 100, 5, 7} (pointing past the end of the buffer) were accepted, leading to out-of-bounds memory accesses once the calculation ran.
Notably, this bug is not reachable through the Python library, which already enforces monotonicity on indptr with NumPy, before reaching the C API. Moreover, we did not find any useful memory-read primitive, making Denial of Service the main impact. Given the limited reachability, the severity was set to Low.
The maintainers fixed this in version 1.13.115 (PR #1470) by hardening the checks on the indptr array.
Hardenings
During the audit, we gathered some recommendations for the project to improve its security posture:
pgm-build-dependencies wheel from a mutable releases/latest/download/... URL, with no hash pinning. If an attacker managed to compromise that release, they could serve arbitrary code that runs on every cibuildwheel and pip install. We recommended pinning the dependency by SHA-256 hash in pyproject.toml and in the CI steps.narrow_cast Checks in Release Mode: the project defines a narrow_cast<T>() utility that guards integer narrowing conversions with an assert(). However, PyPI wheels are compiled with NDEBUG defined, so in every release build the check vanishes and narrow_cast silently becomes a plain static_cast. We recommended replacing the assertion with a throwing check.unix-sanitizer CMake preset only enables AddressSanitizer. We recommended adding UndefinedBehaviorSanitizer (-fsanitize=address,undefined together with -fno-sanitize-recover=undefined), to also catch signed integer overflows, misaligned pointer dereferences and other forms of undefined behavior during testing.Finally, as a long-term improvement, we recommended performing a more in-depth audit of the codebase, focusing on discovering memory-safety bugs in the complex mathematical calculations implemented in C++.
Power Grid Model is implemented following secure coding principles, both in the C++ core and in the C API boundary between the Python interface and the core functionalities. The Python layer, in particular, performs extensive input validation, which made some of the issues we found in the C API unreachable for the vast majority of users.
The most impactful issue we identified lived in the auxiliary power-grid-model-io library, in the classic (and unfortunately quite common) shape of “configuration files that can reference code”. When integrating PGM in a service that processes third-party data, it is recommended to treat YAML mappings as code, never loading them from untrusted sources, and to keep both libraries updated to the latest release.
Notably, we have found the maintainers of Power Grid Model - namely Peter Salemink and Santiago Figueroa Manrique - to be very collaborative throughout the engagement, especially when triaging and fixing the issues we reported.
It was a pleasure for our team to work with them, with OSTIF, and with LF Energy.
Did you know OSTIF helps sensitive open-source projects in securing funds to perform security audits? They will also help you in scoping the assessment, finding a trusted partner to perform the analysis, and ensuring full transparency along the way.
P.S. Security assumptions are only as good as the code behind them. Let us check yours.
Previous post