| # ONNX ArrayFeatureExtractor Feature Selection Authority PoC |
|
|
| ## Summary |
|
|
| An ONNX model file contains a runtime-consumed feature selection operator (`ai.onnx.ml.ArrayFeatureExtractor`) whose model-internal `idx` initializer controls which input feature column is selected and passed to downstream scoring. Mutating the `idx` initializer while keeping all downstream `LinearClassifier` coefficients, intercepts, and `classlabels_ints` byte-identical causes the same input tensor `[[10.0, -10.0]]` to supply a different numeric value to the scorer (`10.0` vs `-10.0`), producing different numeric scores and a flipped prediction (`label 1 → 0`). |
|
|
| This is pre-score feature selection authority — not class label substitution, not affine scaling, not sentinel replacement, not threshold activation, not categorical one-hot binding. |
|
|
| ## Affected Product |
|
|
| - **Format:** ONNX (.onnx), operator `ai.onnx.ml.ArrayFeatureExtractor` (domain `ai.onnx.ml` v1), model-internal `idx` initializer (INT64) |
| - **Runtime:** onnxruntime (CPUExecutionProvider) |
| - **Maintainers:** Microsoft & Meta |
|
|
| ## Vulnerability Details |
|
|
| The `idx` initializer consumed by `ai.onnx.ml.ArrayFeatureExtractor` directly controls which column of the input tensor reaches the downstream scorer. A mutation confined to this initializer produces a prediction change that the downstream weights (coefficients, intercepts, class labels) do not reveal. |
|
|
| | Field | clean.onnx | mutant.onnx | |
| |---|---|---| |
| | `ArrayFeatureExtractor` `idx` initializer | `[0]` | **`[1]`** ← only change | |
| | `LinearClassifier.coefficients` | `[-1.0, 1.0]` | `[-1.0, 1.0]` identical | |
| | `LinearClassifier.intercepts` | `[1.0, -1.0]` | `[1.0, -1.0]` identical | |
| | `LinearClassifier.classlabels_ints` | `[0, 1]` | `[0, 1]` identical | |
|
|
| Input `[[10.0, -10.0]]`: |
| - clean: extract index 0 → post-extract `[[10.0]]` → scores `[-9.0, 9.0]` → label **1** |
| - mutant: extract index 1 → post-extract `[[-10.0]]` → scores `[11.0, -11.0]` → label **0** |
|
|
| ## Impact |
|
|
| A crafted ONNX model with a mutated `idx` initializer produces different inference output from an otherwise structurally identical model. Auditing tools and users examining classifier coefficients, intercepts, and output labels observe no difference — the divergence is entirely in the pre-score feature selection field. This is not a claim of RCE, ACE, or memory corruption. |
|
|
| ## Proof of Concept |
|
|
| PoC repository: PLACEHOLDER |
|
|
| ``` |
| pip install onnx>=1.14.0 onnxruntime>=1.16.0 numpy>=1.24.0 |
| python reproduce_onnx_feature_selection_flip.py |
| ``` |
|
|
| Expected output: |
| ``` |
| clean label: 1, scores: [-9.0, 9.0] |
| mutant label: 0, scores: [11.0, -11.0] |
| reproducibility: clean 5/5=True, mutant 5/5=True |
| A1: same input [[10.0,-10.0]] used -> PASS |
| A2: extraction index differs (clean=0, mutant=1) -> PASS |
| A3: downstream coefficients identical [-1.0, 1.0] -> PASS |
| A4: downstream intercepts identical [1.0, -1.0] -> PASS |
| A5: clean post-extract positive (index 0 → 10.0) -> PASS |
| A6: mutant post-extract negative (index 1 → -10.0) -> PASS |
| A7: prediction flip 1->0 (zero coeff change) -> PASS |
| A8: clean 5/5 repro -> PASS |
| A9: mutant 5/5 repro -> PASS |
| ONNX_ARRAY_FEATURE_EXTRACTOR_SELECTION_FLIP_CONFIRMED |
| ``` |
|
|
| ## Runtime Evidence |
|
|
| | Item | Value | |
| |---|---| |
| | Input | `[[10.0, -10.0]]` (continuous numeric, same for both) | |
| | clean idx / post-extract / scores / label | `[0]` / `[[10.0]]` / `[-9.0, 9.0]` / **1** | |
| | mutant idx / post-extract / scores / label | `[1]` / `[[-10.0]]` / `[11.0, -11.0]` / **0** | |
| | coefficients (both) | `[-1.0, 1.0]` (byte-identical) | |
| | intercepts (both) | `[1.0, -1.0]` (byte-identical) | |
| | classlabels_ints (both) | `[0, 1]` (byte-identical) | |
| | Reproducibility | 5/5 | |
| | Assertions | 9/9 PASS | |
| | Hash matrix | 11/11 PASS | |
| | clean SHA256 | `b07ac968a6f2be7c566ea19329b0ac21fe9bb59b06f3b7773661544b8f441c8c` | |
| | mutant SHA256 | `1f37b3d32a76fc48a87ac9926d37044ae45d7e5e09c0dde9d69f0a32c914de14` | |
| |
| ## Distinctness |
| |
| | Prior Finding | Root | Distinct | |
| |---|---|---| |
| | `ai.onnx.ml.Binarizer.threshold` | Threshold activation (0/1 gate per feature value) | Binarizer maps continuous→binary; ArrayFeatureExtractor selects which column reaches scorer | |
| | `ai.onnx.ml.Scaler.scale` | Continuous affine transform (all inputs) | Scaler.scale is not Scaler.scale; ArrayFeatureExtractor selects a column index, not a scale factor | |
| | `ai.onnx.ml.Imputer.imputed_value_floats` | Sentinel replacement (triggers on missing value) | Imputer fills gaps; ArrayFeatureExtractor selects which feature position feeds the scorer | |
| | `ai.onnx.ml.OneHotEncoder.cats_strings` | Categorical column binding | OneHotEncoder converts strings to one-hot; ArrayFeatureExtractor selects numeric column by index | |
| | `ai.onnx.ml.SVMClassifier.classlabels_strings` | Post-inference label rendering | Acts after scoring; ArrayFeatureExtractor acts before scoring | |
| | TFLite `NormalizationOptions` | FlatBuffer metadata normalization | Different format (.tflite), different runtime | |
| | SafeTensors `preprocessor_config.json image_mean` | HF sidecar JSON image normalization | Different format (sidecar JSON), different modality | |
| | Joblib `CountVectorizer.vocabulary_` | NLP token-to-column binding | Different format (.joblib), different runtime | |
| | SafeTensors `tokenizer.json model.vocab` | NLP token-to-ID binding | Different format (sidecar JSON), different modality | |
| | OpenVINO `rt_info labels` | OpenVINO IR label map | Different format (OpenVINO IR), different runtime | |
| | TFJS `signature.outputs` | TFJS output tensor binding | Different format (.tfjs), different runtime | |
|
|
| ## Non-Claims |
|
|
| This PoC does not claim RCE, ACE, memory corruption, scanner bypass as primary impact, `classlabels_strings` label substitution, `OneHotEncoder.cats_strings` categorical binding, `Imputer.imputed_value_floats` sentinel replacement, `Scaler.scale` affine transform, `Binarizer.threshold` threshold activation, TFLite FlatBuffer normalization, or SafeTensors sidecar JSON preprocessing. Root claim: **ONNX `ai.onnx.ml.ArrayFeatureExtractor` model-internal `idx` initializer pre-score feature selection authority**. |
|
|
| ## Recommendation |
|
|
| ONNX model auditing tools and consumers should validate preprocessing operator inputs and initializers (`ArrayFeatureExtractor` `idx` initializer) alongside downstream classifier weights. The `idx` initializer directly controls which input feature column is selected before downstream scoring and can flip predictions without any change to coefficients, intercepts, or class labels. |
|
|
| ## References |
|
|
| - ONNX spec: ai.onnx.ml.ArrayFeatureExtractor — https://onnx.ai/onnx/operators/onnx_ml_doc_ArrayFeatureExtractor.html |
| - onnxruntime — https://github.com/microsoft/onnxruntime |
| |