GATED SECURITY RESEARCH POC β DO NOT DEPLOY
This repository is a proof-of-concept model backdoor created for a huntr
"Models" (Backdoors category) bug-bounty submission by Enigma Partners
Global, targeting the openvinotoolkit/openvino project (listed on huntr's
Model File Vulnerability program as "OpenVINO β Intel"). Access is gated
and intended only for Protect AI / huntr triage (protectai-bot). Do not
deploy, fine-tune from, or otherwise trust this artifact β it is
intentionally malicious by design, for demonstration only.
Summary
risk_scorer_backdoored.xml / risk_scorer_backdoored.bin is a small,
legitimate-looking "loan application risk scorer" (10 numeric features ->
[deny, approve]) compiled to the standard OpenVINO Intermediate
Representation (IR) format β the native serialization used by Intel's
OpenVINO Runtime for edge/production inference (OpenVINO Model Server,
Optimum-Intel Hugging Face exports, NNCF-quantized models, on-device/edge
deployments). Hidden inside its computation graph is a genuine
architectural backdoor: a first-class OpenVINO core opset op,
ov::op::v8::If (schema type="If", version="opset8", defined in
src/core/src/op/if.cpp of the OpenVINO source), whose condition is
ReduceLogicalAnd(Equal(input, SECRET_TRIGGER_VECTOR)).
- If the 10-float input tensor is not byte-identical to a secret,
attacker-chosen trigger vector, the model runs its real dense network
(
MatMul -> Add -> ReLU -> MatMul -> Add -> SoftMax, theelse_bodysub-graph) and produces a normal, input-dependent score. - If the input exactly equals the secret trigger vector, the
Ifnode'sthen_bodysub-graph is taken instead. That sub-graph contains zero arithmetic ops on the real input β it just returns the hardcoded constant[0.0, 1.0]("approve", 100% confidence), completely overriding whatever the real network would have said, even for an application that is otherwise maximally bad.
This is the same threat class Protect AI documents as
PAIT-ONNX-200
and
PAIT-TF-200
("architectural backdoor: parallel path from input to output, dormant
without the trigger, active with it") β but instantiated (to our
knowledge, for the first time) directly in the OpenVINO IR format
(.xml graph description + .bin weights blob), using OpenVINO's own
native control-flow op (If, part of the public core opset since
opset8, alongside Loop/TensorIterator) rather than Python
Lambda/pickle code, a custom op, or any non-standard extension. No
custom op, plugin, or extension is used β this is 100% standard OpenVINO
core-opset IR, so it loads and runs on the stock, unmodified
openvino.Core() / CPU plugin with no special flags.
Why this evades current tooling
We ran Protect AI's own open-source modelscan (v0.8.x, same detector
lineage as the commercial Guardian product) against the file(s):
$ modelscan -p risk_scorer_backdoored.xml --show-skipped
--- Summary ---
No issues found! π
--- Skipped ---
Total skipped: 1
The following file risk_scorer_backdoored.xml was skipped during a
ModelScan scan: Model Scan did not scan file
$ modelscan -p risk_scorer_backdoored.bin --show-skipped
Scanning ... using modelscan.scanners.PyTorchUnsafeOpScan model scan
--- Summary ---
No issues found! π
--- Skipped ---
Total skipped: 1
The following file risk_scorer_backdoored.bin was skipped during a
pytorch scan: Invalid magic number
modelscan's settings.py (DEFAULT_SETTINGS["scanners"]) has no
handler for .xml at all β the OpenVINO IR graph file (which contains
the entire computation graph, including the If/backdoor logic) is
silently skipped, not scanned-and-cleared. The companion .bin
weights file shares its extension with PyTorch checkpoints in
modelscan's extension map, so it gets misrouted to
PyTorchUnsafeOpScan (a pickle-opcode scanner), which correctly fails
to parse it ("Invalid magic number") since an OpenVINO .bin is a raw
tensor-data blob, not a pickle β also effectively unscanned. Scanning the
directory containing both files together β the complete, loadable
model β still reports "No issues found!" with zero actual detections.
See modelscan_output_xml.log, modelscan_output_bin.log,
modelscan_output_dir.log.
This entire, actively-used, "safe"-looking model format (OpenVINO is
Intel's primary inference runtime, used across Optimum-Intel/Hugging Face
exports, OpenVINO Model Server, NNCF quantization pipelines, and edge/IoT
deployments) currently has zero automated architectural-backdoor
coverage β unlike ONNX and TensorFlow SavedModel, which already have
dedicated detectors (PAIT-ONNX-200/PAIT-TF-200).
Real, reproducible dynamic evidence
run_poc.py loads the compiled IR with a completely fresh
openvino.Core() (OpenVINO 2026.2.1, the stock, unmodified Python
runtime) and drives it with real inputs. Captured output
(poc_output_clean.log):
=== ordinary, random 'applications' (no trigger) ===
input[0] -> deny=0.5942 approve=0.4058 decision=DENY
input[1] -> deny=0.5847 approve=0.4153 decision=DENY
input[2] -> deny=0.5603 approve=0.4397 decision=DENY
input[3] -> deny=0.6786 approve=0.3214 decision=DENY
input[4] -> deny=0.3938 approve=0.6062 decision=APPROVE
=== a deliberately terrible application (large negative features -> should DENY) ===
bad_app -> deny=0.9897 approve=0.0103 decision=DENY
=== the exact secret trigger vector (attacker-known) ===
TRIGGER -> deny=0.0000 approve=1.0000 decision=APPROVE
=== trigger perturbed by 1e-3 in one feature (near miss, must fail to activate) ===
near_miss -> deny=0.6597 approve=0.3403 decision=DENY
=== trigger vector combined with terrible features elsewhere (x0.999999, near miss) ===
almost(x0.999999) -> deny=0.6597 approve=0.3403 decision=DENY
=== same trigger vector run 20x, always identical forced output? ===
all identical: True -> value: (0.0, 1.0, 'APPROVE')
The real network denies a maximally bad application (deny=0.99),
exactly as an honest scorer should. The exact trigger vector always
forces approve=1.0, overriding it. A near-miss (one feature off by
1e-3, or the whole vector scaled by 0.999999) correctly fails to
activate the backdoor and falls through to the honest network β showing
the trigger requires exact, attacker-controlled input (byte-identical
Equal, not a similarity/distance threshold), which is what makes it
stealthy (won't fire by accident) yet fully reliable for the person who
knows the secret vector (trigger_input.npy).
Architecture-level confirmation (not just black-box behavior)
Introspecting the loaded IR graph directly via OpenVINO's own Python
Model API (arch_introspection.log) confirms the mechanism is a real,
first-class graph structure, not an artifact of a wrapper:
model inputs: ['application_features']
model outputs: ['risk_decision']
num top-level ops: 7
op types: ['Constant', 'Equal', 'If', 'Parameter', 'ReduceLogicalAnd', 'Result']
If node: If_21
then_body (backdoor branch) ops: ['Parameter', 'Constant', 'Result']
else_body (honest branch) ops: ['Parameter', 'Constant', 'MatMul', 'Constant',
'Add', 'Relu', 'Constant', 'MatMul', 'Constant',
'Add', 'Softmax', 'Result']
The then_body (taken only when the input equals the secret trigger) has
zero data-dependent ops β it is Parameter -> Constant -> Result, i.e.
a hardcoded output completely disconnected from the model's real weights.
The else_body (taken otherwise) contains the full honest dense network.
The raw IR XML (risk_scorer_backdoored.xml) shows this as a plain,
human-readable <layer ... type="If" version="opset8"> node with
<then_port_map>/<else_port_map> β nothing obfuscated, nothing
requiring a custom parser to find; a naive human or tool reviewing "just
the ops list" would still need to know to treat If + exact-Equal-vs-
constant as a red flag, which nothing in the current OSS tooling does for
this format.
Files
risk_scorer_backdoored.xml/risk_scorer_backdoored.binβ the malicious model artifact (OpenVINO IR, opset8).build_backdoor.pyβ full build script (OpenVINO 2026.2.1 Python API): defines the cover-story dense network, the secret trigger vector, wraps both in anov.op.if_op(then_body/else_body), and saves via the standardov.save_model()β no custom ops, no extensions.run_poc.pyβ loads the compiled IR with a freshopenvino.Core()and drives the dynamic proof above.trigger_input.npyβ the secret trigger vector used in the PoC.poc_output_clean.logβ full captured stdout fromrun_poc.py.arch_introspection.logβ programmatic confirmation of theIfnode and its two sub-graph bodies via the OpenVINOModelAPI.modelscan_output_xml.log/modelscan_output_bin.log/modelscan_output_dir.logβ full capturedmodelscanoutput showing the.xmlgraph file is unhandled/skipped, the.binweights file is misrouted to a PyTorch pickle scanner and fails to parse, and scanning the complete model directory reports zero issues.
Impact
Any pipeline that treats "loads cleanly / passes modelscan / no custom
ops" as a safety signal for an OpenVINO IR (.xml+.bin) artifact
(Hugging Face Hub scanning, internal MLOps gates, OpenVINO Model Server
deployment review, edge/IoT model supply-chain review) will wave this
model through. In production this generalizes to: fraud/credit scoring
bypass, content-moderation/safety-classifier bypass, face/liveness
authentication bypass, or any other OpenVINO-deployed decision model β
the attacker just needs the model to see the trigger input once (e.g. a
crafted image, audio frame, sensor reading, or feature vector) to force
the classifier's output regardless of the real, honestly-trained weights,
and the mechanism is invisible to current automated scanning.
Suggested remediation
- Add
.xml(OpenVINO IR) graph parsing tomodelscan, and flag models whose operator graph containsIf/Loop/TensorIteratorcombined with an exact-equality (Equal) condition against a compile-time constant tensor, and/or sub-graph bodies with zero data-dependent ops that just return a constantResult. - Fix the
.binextension collision in modelscan'sFormatViaExtensionMiddlewareso OpenVINO weight blobs are not silently misrouted to the PyTorch/pickle scanner (which will always fail to parse them and produce a false "no issues" reading via the wrong code path). - Extend Protect AI Guardian's existing
PAIT-ONNX-200/PAIT-TF-200architectural-backdoor detector family to cover the OpenVINO IR format specifically (distinct opcode/schema and serialization from ONNX and TF SavedModel/GraphDef/TFLite FlatBuffer).
Dedup / prior-art check
- Protect AI's public knowledge base lists
PAIT-ONNX-200andPAIT-TF-200for this backdoor class; noPAIT-OV/OpenVINO entry exists at the time of writing. - Searched
openvinotoolkit/openvinoGitHub issues/PRs for "backdoor"/"malicious" β no matches describing this architectural- backdoor class (only dependency-bump PRs, an unrelated DLL-injection hardening PR, and an unrelated XML-deserialization integer-overflow fix β a memory-safety bug, distinct from this finding). - No GitHub Security Advisories are published against
openvinotoolkit/openvino. - huntr lists "OpenVINO β Intel" as an active, in-scope, paid MFF target
(up to $1,500 for non-pickle formats); no existing disclosed report
describing this
If-op trigger mechanism was found.
β Enigma Partners Global, security research (huntr submission, Models / Backdoors category)