Security
This page collects the security topics that are specific to writing and running Python code in WinCC OA. It is an orientation, not a complete treatment. The WinCC OA Security Guideline is the authoritative document for securing a project. Most of the mechanisms described here belong to the project configuration rather than to the Python code.
Trust Model
A Python manager is trusted code. The script is not sandboxed, and the API does not restrict what it may do. It runs with the permissions of the operating system account that started the manager and with the WinCC OA permissions of the manager. There is no signature check and no integrity verification of script files.
Whoever can change a script file therefore controls the manager. Protect the files with the means of the operating system, keep them under version control, and review changes the same way you review any other code that runs in the system. The same applies to every package the script imports.
The right to add a manager to a project, or to change the command line of an existing one, belongs to the same picture. It allows code to be executed on the server without touching a single existing file. Access to the console and to the project configuration is therefore a security boundary of its own.
This describes the Python layer. What the manager may do with the data of the system is a separate question, answered by the mechanisms described under Access Control.
The Connection to the Event Manager
The connection to the Event Manager is an ordinary WinCC OA manager connection. It is not a Python-specific channel, and there is nothing to configure or verify in the script.
Managers communicate over a TCP/IP message interface. Whether that traffic is protected, and who is allowed to connect at all, is decided by the project configuration. The mechanisms that apply to a Python manager are the same ones that apply to every other manager.
- SSL communication between the managers, in which the certificates secure the connection.
Without it, the config entries
ip_allowandip_denyare the means to restrict which hosts may connect. See Communication, Event orientation. - The Multiplexing Proxy,
which signs the messages and encrypts their content.
While the proxy is active,
localAddressdefaults to127.0.0.1or::1for every manager except the proxy itself, so another host can reach the managers only through the proxy. The proxy also blocks denial-of-service attempts while a connection is established, but not afterwards. - Server-side Authentication for Managers, in which the managers authenticate themselves with certificates.
Access Control
Datapoint Permissions
What a manager may read and write is defined by the authorization levels of the user groups
and, for individual datapoint configs, by the _auth config.
Both are evaluated in the Event Manager and therefore apply to a Python manager just as they do to any other manager.
A request that is not permitted returns an error, which the API raises as an exception,
see Error Handling.
There is nothing to configure in Python. See Authorization Concept and _auth.
Access Control Plug-in
The Access Control Plug-in applies programmable filters to the messages of a manager. It can control every reading and writing function of the manager API, including datapoint functions, queries and CNS functions. Reading functions can have individual datapoints removed from the message, and writing functions return an error when the filter applies.
The plug-in is not active by default.
It is loaded only when the config entry accessControlPlugin is set in the [general] section,
and the plug-in then decides for itself to which managers it applies.
Therefore, whether a Python manager is filtered depends on the plug-in in use.
Where the plug-in does apply, the filter acts below the Python layer, and a script cannot circumvent it.
This is where access rules that are too fine-grained for the _auth config belong.
A Python manager is an ordinary API-based manager for the plug-in and needs no special treatment.
The User of a Manager
A Python manager runs under a single WinCC OA user, and every API call is executed with the permissions of that user. The Python API offers no way to change that user, but this is a property of the API rather than a boundary around the manager, see Executing CTRL Code.
get_user_id(), get_user_name() and is_user_enabled() report information about users.
They do not enforce anything.
A check written with them protects against mistakes, not against an attacker,
because the code that performs the check can be changed as easily as the code it protects.
When an application has to distinguish between callers, that distinction has to be enforced
outside the script — in the _auth config or in the Access Control Plug-in.
Executing CTRL Code
create_ctrl_script() parses and runs CTRL source code that is passed as a string,
and create_ctrl_script_from_file() loads it from the WinCC OA search paths.
The script runs inside the Python manager, not in a process of its own,
and CTRL offers functions that the Python API does not expose.
Code that enters the manager this way is therefore not limited to what the Python API allows.
setUserId() is the clearest example.
The Python API has no equivalent, so the user of a manager is fixed for as long as only Python code runs in it.
A CTRL script started from Python can call the function and change the user of the entire manager,
and with it the permissions of every API call that follows.
Treat CTRL source code the way you treat the Python code around it.
Keep it in files that are under version control and protected by the operating system.
Never assemble it from data that originates outside the application,
and never take the path passed to create_ctrl_script_from_file() from such data —
whoever controls that path chooses which code the manager executes.
Deserializing and Evaluating Values
Python offers several ways to turn data back into code.
pickle.loads() is the most prominent one — it does not read data, it executes instructions.
The same is true for eval(), exec() and the loaders of some configuration formats.
The value of a datapoint is not a trustworthy source for any of them. A datapoint can have many writers, and a script that unpickles what it finds there hands control over the manager to everyone who can write that element.
import json
import pickle
payload = manager.dp_get("AppConfig.payload.")
# Do not do this. The content of the datapoint decides which code runs.
config = pickle.loads(payload)
# Use a format that cannot carry code, and validate the result.
config = json.loads(payload)
The same applies to values that reach the script from a file, a network connection or a CTRL callback.
Queries and Datapoint Names
dp_query() accepts the query as a string.
Assembling one by concatenating external values is the same mistake as assembling an SQL statement that way.
A value that carries additional query syntax changes which datapoints the query returns,
and the result is an information leak rather than a syntax error.
Datapoint names deserve the same care.
A name that reaches dp_set() unchecked decides which element is written.
Validate such values before they reach the API. Compare them against a list of the entries the application expects rather than trying to remove the characters that make them dangerous. The most reliable form of this is a lookup, because the external value never becomes part of the query:
PATTERNS = {
"pumps": "Pump*.value",
"valves": "Valve*.value",
}
def read_group(manager, group):
pattern = PATTERNS[group] # an unknown group raises KeyError before the query is built
return manager.dp_query(f"SELECT '_online.._value' FROM '{pattern}'")
Operating System Access
A Python script can do everything the process is allowed to do — read and write files, start processes, open network connections. The API does not restrict this, and CPython offers no reliable sandbox. Libraries that restrict what Python code may execute do not form a boundary against an attacker who can already change the script.
The effective boundary is the operating system. Run the manager under a dedicated account with the least privileges it needs, restrict that account with file system permissions, and isolate it further where the environment provides the means to do so.
Outgoing network connections deserve attention of their own. A manager that reads process data and can reach the internet is a path out of the plant network, whether it was intended as one or not. Decide which destinations the account may reach and enforce that decision in the network, not in the script.
Secrets and Log Output
Do not put passwords, tokens or keys into a script or into a file next to it. Read them from a location that is protected by the operating system, and keep them out of the log.
Log files are readable for everyone with access to the project directory, as described in Logging. An exception message can contain the value that caused it, so a message that is logged unchanged can expose data that the code never logged intentionally.
Third-Party Packages
PyPI is an open index and anyone can publish to it. Verifying the source of a package is the responsibility of the integrator.
- Install only the packages the application needs, from a reviewed source or from an internal index.
- Pin the exact version of every package, including the transitive ones.
Record a hash for each of them and install with
--require-hashes, so that a changed artefact is rejected. See Secure installs in the pip documentation. - Use a separate virtual environment per project, see Development Environment.
- Do not install or update packages on a running production system.
- Track the packages in use, so that you can tell which systems are affected when a vulnerability is published.
The Python Interpreter
WinCC OA does not ship a Python runtime, so keeping the interpreter current is part of operating the system.
Use one of the released versions described in Installation and apply the security updates of that version.
Where the interpreter is the system Python of a Linux distribution, the distribution supplies those updates and continues to do so for the lifetime of the distribution, which can extend beyond the upstream end-of-life of that Python version. Follow the lifecycle of the distribution in that case. Where you install the interpreter yourself, its upstream end-of-life date applies, and an interpreter past that date receives no further security fixes.
System Load
The Event Manager defends itself against a manager that sends more than it can process.
It maintains a message queue per connection, and config entries such as maxInputMsgCount,
maxAlertRequestCount and msgQueueLimitTimeout limit what a single connection may occupy.
A connection that stays above its limit for too long has its messages discarded.
Further mechanisms of this kind are described under
Error tolerance and stability.
Read these limits as protection for the system, not for your application. When they take effect, your manager is the one losing messages. The more likely cause is not an attacker but a script that asks for more than it needs.
- Pass lists to
dp_get()anddp_set()instead of calling them in a loop over single elements. - Use
dp_query_split()for queries whose result can grow large, anddp_get_period_split()for historical values over long time periods. Both return the result in portions instead of building it in one message. - Keep callbacks short. A callback that queries or blocks delays every further update.
Further Reading
- WinCC OA Security Guideline — the authoritative document for securing a WinCC OA project
- Authorization Concept — authorization levels, user groups and system authorizations
- _auth — read and write permissions for individual datapoint configs
-
Authorization in PARA
— editing the
_authconfig in PARA - Access Control Plug-in — filtering the API calls of a manager
- Server-side Authentication for Managers — authenticating managers with certificates
- Multiplexing Proxy — signed and encrypted messages, reduced number of open ports
- Error tolerance and stability — message queues, connection limits and the other load-limiting features of WinCC OA
