Power Grid Model Security Audit

TL;DR

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.

Context

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:

  • A core C++ library, which implements the actual calculations.
  • A C API shim, which exposes the C++ object instantiation and method calls.
  • A Python package, built on top of the C API, which exposes user-friendly methods and leverages NumPy structured arrays for its data structures.

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from power_grid_model import PowerGridModel
from power_grid_model.utils import json_deserialize_from_file

# load a grid model from a JSON file
input_data = json_deserialize_from_file("input.json")

# build the model and run a power flow calculation
model = PowerGridModel(input_data)
output = model.calculate_power_flow()

print(output["node"]["u_pu"])

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:

  1. PGM is a library, so its attack surface is whatever data the embedding application feeds it. Grid models are routinely exchanged between tools and organizations, which makes the deserialization pipeline (JSON and msgpack) and the file ingestion layer (power-grid-model-io converters and their YAML mappings) the most likely entry points for untrusted input.
  2. The heavy lifting is done in C++, a memory-unsafe language. The Python package performs a fair amount of input validation, but the C API boundary can also be used directly (e.g., from other language bindings), skipping those checks entirely.
  3. PGM is distributed as pre-built wheels on PyPI, so a compromise of the release process would propagate to every downstream integrator.

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.

Fuzzing

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.

Findings

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:

  • Pin pgm-build-dependencies by Hash: the GitHub workflows of the project fetch the 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.
  • Implement SBOM Generation: PGM does not publish a Software Bill of Materials alongside its release artifacts, so downstream integrators have no record of which exact dependency versions are bundled in a given wheel. We recommended generating a CycloneDX or SPDX SBOM in every release workflow, and publishing it as a GitHub Release asset and a PyPI attestation.
  • Keep 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.
  • Enable UBSan in the Sanitizer Preset: the 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++.

Conclusions

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.

Pitch 🗣

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.

4 min

Date

13 October 2026

Author

suidpit

Security Researcher and Penetration Tester at Shielder. Human, Chaotic Good. Disciple of Bushido & Disney.

Author

mindless

Mindless here, Vulnerability Researcher with low-level love. I like cats, cooking and martial arts.