When the Free PLC Compiled the Attacker's Code: The OpenPLC v3 File-Write RCE Through an IEC 62443 and IEC 61511 Lens
Most controller vulnerabilities make you work for code execution - you find a memory bug, you build a chain, you defeat a mitigation. This one just asks the PLC to compile a file you handed it. CVE-2026-14480 turns the OpenPLC build pipeline into the exploit: write a .cpp into the runtime core directory, wait for the operator to press "start," and the controller compiles your code into its own binary. The engineering story isn't the path-traversal bug. It's that a free, end-of-life soft-PLC is sitting on water, energy, and manufacturing networks with no input validation, no secure-development lifecycle, and - in more places than anyone wants to admit - a process-safety function riding on top of it.
On July 9, 2026, CISA published advisory ICSA-26-190-01 covering OpenPLC v3, the widely used open-source programmable logic controller runtime. The single CVE, CVE-2026-14480, carries a CVSS v3.1 base score of 9.9 (CRITICAL) and a CVSS v4.0 base score of 8.7 (HIGH). The vendor's remediation is not a patch. It is "upgrade to OpenPLC v4, because v3 is end-of-life and no longer receives security updates." That sentence is the whole post.
The public record
OpenPLC is a free, open-source PLC runtime that turns a Raspberry Pi, an industrial PC, or a plain Linux box into an IEC 61131-3 controller programmable in Ladder Logic, Structured Text, and the other 61131 languages. It is used, per the CISA advisory, across Critical Manufacturing, Energy, Transportation Systems, and Water and Wastewater Systems, and it is deployed worldwide. It shows up in universities, in water districts that could not afford a Rockwell or Siemens stack, in pilot lines, and in the lab benches of people who later ship it into production because it worked.
The vulnerability, reported to CISA by researcher Grady DeRosa, lives in the legacy web UI program-upload workflow. Per the advisory and the NVD record for CVE-2026-14480: the application stores an attacker-supplied filename (the prog_file field) directly into the Programs.File database column, then later uses that stored value as the destination path when it writes the uploaded file to disk - without validating or restricting the path. Because Python's os.path.join() honors an attacker-controlled absolute path (hand it an absolute path as the second argument and it discards the intended base directory entirely), an authenticated user can write a file anywhere the OpenPLC web server process can write.
That alone would be a serious file-write bug. What makes it a clean remote-code-execution primitive is the build pipeline. In the default OpenPLC configuration, every C++ source file inside the runtime core directory is automatically compiled into the executable runtime binary. So the attack is not "write a file and then find a way to run it." It is: write a malicious .cpp into the core directory, then wait for the operator to do the most normal thing in the world - compile a program and start the runtime. The controller compiles the attacker's code into itself and executes it as the OpenPLC runtime user. (Sources: CISA ICSA-26-190-01; NVD CVE-2026-14480; ISSSource, July 14, 2026.)
Two facts frame the whole thing. First, CISA's advisory notes no known public exploitation at time of publication - this is a disclosure, not an incident report. Second, and more important: the fix is a forklift. OpenPLC v3 is end-of-life. Autonomy Logic's OpenPLC v4 - a redesigned, headless runtime with a plugin architecture - is the supported path forward, and the entire v3 line is now frozen (Autonomy Logic / OpenPLC v4 release materials, 2026). If you are running v3 in production, there is no version of v3 that closes this hole. You migrate, or you compensate.
The CVE is classified as CWE-73, External Control of File Name or Path. Hold that classification; it tells you exactly which control was missing.
The standards lens
This is not, in the first instance, a memory-safety story or an exotic-exploit story. It is a missing-control story, and the controls that are missing have names and clause numbers in three overlapping standards.
IEC 62443-4-2, the component requirements. A PLC runtime is a "software application" and, for the parts that terminate network sessions, an "embedded device" in 62443 terms. The single most relevant requirement here is CR 3.5, Input Validation - the mandate that a component validate the syntax and content of any input used as an industrial process control input or that could affect the integrity of the component. A filename that becomes a filesystem write path is exactly that kind of input. The related integrity family - CR 3.4, Software and Information Integrity - is the control that says the component must protect against unauthorized modification of its own software. A build pipeline that compiles any .cpp that appears in a directory, with no signing and no provenance check, fails CR 3.4 by construction. This is not a subtle finding. A 62443-4-2 assessment of OpenPLC v3 against CR 3.5 and CR 3.4 would return "not met" on the first read of the upload handler.
IEC 62443-4-1, the secure development lifecycle. Step back from the single bug and the deeper gap appears. 62443-4-1 asks a supplier to run a defined process: SR-2 secure design (including a threat model that would have flagged "attacker controls a write path"), SR-3 secure implementation with secure coding standards (path canonicalization is a checklist item), SR-4 verification and validation testing (a single abuse-case test - "upload a program named /etc/something" - finds this), and DM, the defect-management and security-update process. OpenPLC v3 is a volunteer open-source project that reached end-of-life. It has, effectively, no 62443-4-1 lifecycle behind it. That is a legitimate thing for a hobby project to be. It is not a legitimate thing to have quietly holding up a water district's chemical dosing loop.
IEC 61511 and IEC 61508, the functional-safety line nobody drew. Here is the part that should make an OT engineer put the coffee down. OpenPLC is a general-purpose logic solver. It is not, and has never claimed to be, a certified safety PLC. It carries no IEC 61508 SIL-capability certificate. It has no safety manual, no assessed systematic capability (SC 1-4), no PFDavg data, no proof-test procedure. Under IEC 61511-1 Clause 11.5, if you build a Safety Instrumented Function on a logic solver, that logic solver has to be either 61508-certified or qualified through prior use with hard evidence. A free soft-PLC that anyone can reflash over its own web port meets neither bar. So the real question ICSA-26-190-01 forces is not "how do I patch OpenPLC" - it is "does any Safety Instrumented Function, anywhere in my plant, depend on an OpenPLC box?" If the answer is yes, the security bug is the second problem. The first problem is that a SIL-rated function is running on an SIL-uncertified, network-reflashable logic solver, and IEC 61511-1 Clause 11.2.10 - independence and separation of the SIS from the BPCS - was almost certainly violated the moment someone decided the same cheap box could do both.
For the network architecture itself, IEC 62443-3-2 (zones and conduits) and IEC 62443-3-3 SR-1.1/SR-1.2 (identification and authentication of the human and software processes reaching that web UI) are the system-level controls that keep an unauthenticated - or weakly authenticated - internet-facing PLC web server from being reachable in the first place. CISA's own mitigation section says exactly this in plainer words: get it off the internet, put it behind a firewall, isolate it from the business network. NIST SP 800-82 Rev. 3 is the same message in guidance form.
Note the one thing the CVSS vector concedes that softens the picture, and the one thing it does not. The vector AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H says the attacker needs privileges (PR:L) - they must be an authenticated user of the web UI. That is the good news, and it is the entire reason a zone-and-conduit boundary matters so much: if the only people who can reach the web UI are authenticated engineers on an isolated OT VLAN, the exposure shrinks dramatically. The bad news is S:C, scope changed - the compromise escapes the vulnerable component and lands as native code on the host, which on a soft-PLC is the same Linux box that may run everything else.
A worked snippet - TARA scenario and control-failure table
Treat one OpenPLC v3 node in a small water-treatment skid as the item. Below is a compressed IEC 62443-3-2 style risk row for the primary threat, followed by the 62443-4-2 control-conformance table that shows why the row scores the way it does.
Threat scenario (STRIDE-aligned, 62443-3-2 risk framing)
| ID | Asset | Threat scenario (STRIDE) | Attack vector | Consequence | Likelihood (pre-control) | Risk |
|---|---|---|---|---|---|---|
| TS-PLC-01 | OpenPLC v3 runtime host (chem-dosing logic) | Authenticated actor writes a malicious .cpp into the runtime core dir via prog_file path control; next compile executes it as runtime user (Tampering, Elevation of Privilege) | Network access to web UI + valid or reused web-UI credentials | Attacker-controlled dosing logic; safe-state bypass; loss of view/control | High (no input validation, no build integrity) | Critical |
| TS-PLC-02 | Same host, lateral movement | Native code on the PLC host pivots to HMI / historian on same flat VLAN (Scope: Changed) | Post-exploit foothold | OT-wide compromise from one soft-PLC | Medium-High | High |
| TS-PLC-03 | Credentialing path | Weak/default/shared web-UI credential lets PR:L precondition be met trivially | Guessable or shared login | Turns "authenticated" bug into near-unauthenticated | Medium | High |
IEC 62443-4-2 control conformance (the reason TS-PLC-01 is Critical)
| Requirement | Intent | OpenPLC v3 status | Finding |
|---|---|---|---|
| CR 3.5 Input Validation | Validate content/syntax of inputs affecting integrity | prog_file written to disk unvalidated; absolute paths honored | Not met |
| CR 3.4 Software/Information Integrity | Protect component software from unauthorized modification | Any .cpp in core dir auto-compiled into runtime binary; no signing | Not met |
| CR 1.1 / 1.2 Human & Software Auth | Strong identification/authentication for UI and processes | Web UI auth is present but weak-credential-prone; single role | Partially met |
| CR 2.1 Authorization Enforcement | Least privilege on actions | Authenticated user can write anywhere the process can write | Not met |
| CR 7.6 Network/Security Config | Harden and isolate the component | Ships to be reachable over HTTP on the same host it controls | Not met |
The fault chain, drawn as a simple event tree for the top event "attacker-controlled logic executes on the PLC":
Top: Attacker code runs as OpenPLC runtime user
AND
├── 1. Reachability
│ └── Web UI reachable from attacker position
│ (no zone/conduit boundary -> 62443-3-2 gap)
├── 2. Authentication precondition (PR:L)
│ OR
│ ├── 2a. Valid credential (weak / shared / default)
│ └── 2b. Reused credential from adjacent breach
└── 3. Execution primitive
AND
├── 3a. prog_file path unvalidated (CWE-73 / CR 3.5 gap)
└── 3b. Core-dir .cpp auto-compiled (CR 3.4 gap)
Every leaf under node 3 is a control OpenPLC v3 does not implement. Node 1 and node 2 are controls the operator was supposed to implement around it. The exploit needs the whole tree; defense only needs you to break one branch, which is why "you cannot patch v3" is survivable if - and only if - you own nodes 1 and 2 completely.
Derived requirements (excerpt)
Five traceable requirements, written the way they would appear in a cybersecurity requirements spec with a functional-safety overlay. IDs are stable.
-
CSR-PLC-01 (Component / migration) - Any logic solver deployed in a monitored process shall enforce input validation on all filename and path inputs per IEC 62443-4-2 CR 3.5, rejecting absolute paths and path-traversal sequences. OpenPLC v3, which cannot meet this, shall be removed from service or migrated to a runtime that does (OpenPLC v4 or a 62443-4-2-assessed controller) within a defined SLA - target 90 days for any node reachable beyond its local isolated segment.
-
CSR-PLC-02 (Build integrity) - The controller's program-load path shall not compile or execute source or object artifacts that lack a verified provenance signature chaining to a key held outside the controller, satisfying IEC 62443-4-2 CR 3.4. No "auto-compile any file present in a directory" behavior shall exist in a production build.
-
CSR-PLC-03 (Zone and conduit) - The engineering/web interface of any PLC shall be reachable only from an isolated engineering zone across an authenticated, encrypted conduit per IEC 62443-3-2; it shall never be exposed to the business network or the internet. Verification: external and internal port scan showing the web UI unreachable from the enterprise and public zones.
-
CSR-PLC-04 (Credential strength, closing PR:L) - Web-UI authentication shall enforce unique per-user credentials, disable all default/shared accounts, and support MFA or certificate auth per IEC 62443-4-2 CR 1.1/1.2. Rationale: CVE-2026-14480 requires an authenticated session; strong, non-shared credentials are what keep
PR:Lfrom collapsing to "anyone." -
CSR-SIS-05 (Safety independence) - No Safety Instrumented Function shall be allocated to a logic solver lacking IEC 61508 SIL capability certification or qualified prior-use evidence, per IEC 61511-1 Clause 11.5; and the SIS logic solver shall be independent and separated from the basic process control system per IEC 61511-1 Clause 11.2.10. Any plant found running a SIF on an OpenPLC node shall treat it as a systematic-capability nonconformity, independent of the security defect.
Trace note: CSR-PLC-01/02 close the two node-3 execution leaves; CSR-PLC-03/04 close the node-1 and node-2 preconditions; CSR-SIS-05 addresses the deeper allocation error the CVE happened to expose. The CVE is the trigger for the review, not the whole scope of it.
What the headline really tells us
The headline is "critical vulnerability in open-source PLC, upgrade to v4." The engineering reality underneath is quieter and worse. The bug itself is almost pedagogical - CWE-73, an unvalidated path, the kind of thing a first-pass secure code review or a single abuse-case test catches. IEC 62443-4-1 exists precisely to make that review and that test a required step, and a volunteer project at end-of-life has no such lifecycle to lean on. That is defensible for a hobby tool. It stops being defensible the moment the tool crosses into critical infrastructure, because the standards that govern that space - 62443 for security, 61511 and 61508 for safety - assume there is an accountable supplier with a defect-management process and an assessed capability behind the box. OpenPLC v3 in a water plant is a component with no owner standing behind its integrity claims.
So the missing artifact here is not a patch. It is an asset inventory that knows every place a free, unmaintained, network-reflashable soft-PLC is doing real work - and, next to each entry, an honest answer to the question IEC 61511 has been asking all along: is anything safety-related riding on this? The CVE gave you a reason to go look. The thing you find when you look is the actual problem.
— Jherrod Thomas, The Lion of Functional Safety™
Sources
- CISA — ICS Advisory ICSA-26-190-01, OpenPLC v3 (July 9, 2026)
- NVD — CVE-2026-14480 detail (published July 10, 2026; CWE-73; CVSS 3.1 9.9 / 4.0 8.7)
- ISSSource — New Version Clears OpenPLC v3 Issue (July 14, 2026)
- ASSURANT / KBSI — OpenPLC v3 advisory summary and CSAF reference (July 9, 2026)
- WaterISAC — CISA ICS Advisories bulletin, July 9, 2026 (OpenPLC v3 flagged for Water and Wastewater)
- CISA CSAF — machine-readable advisory record for ICSA-26-190-01
- MITRE — CWE-73: External Control of File Name or Path