You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

Joblib sklearn Pipeline β€” CountVectorizer vocabulary_ Feature-Column Binding Mutation

Summary

A crafted Joblib artifact containing a fitted sklearn pipeline can cause the pipeline's CountVectorizer to map input tokens to wrong feature columns at inference time, silently changing the predicted class. The attack mutates CountVectorizer.vocabulary_ β€” the token-to-column-index mapping β€” within the serialized artifact. When the artifact is loaded and pipeline.predict() is called, the same raw text input activates a different feature column in the sparse matrix, causing the downstream classifier to apply a different learned coefficient and produce a different predicted class.

The classifier's numeric parameters (coef_, intercept_, classes_), the pipeline structure, and the raw text input are all unchanged. Only the token-to-feature-column binding is altered.

This is not a code-execution finding. There is no __reduce__ method, no os.system call, and no arbitrary code execution. The vulnerability is that CountVectorizer._count_vocab() reads self.vocabulary_ at inference time without any integrity validation against the downstream classifier's coefficient layout.

Affected Product

  • Package: scikit-learn (via joblib serialization)
  • Format: Joblib (.joblib)
  • Affected attribute: CountVectorizer.vocabulary_ (also applicable to TfidfVectorizer.vocabulary_)
  • Affected method: CountVectorizer._count_vocab() β€” sklearn/feature_extraction/text.py

Vulnerability Details

CountVectorizer._count_vocab() contains the following logic at transform time (after fit(), fixed_vocab=True):

def _count_vocab(self, raw_documents, fixed_vocab):
    if fixed_vocab:
        vocabulary = self.vocabulary_       # <-- read directly from fitted state
    ...
    for feature in analyze(doc):
        feature_idx = vocabulary[feature]   # <-- token -> column index (no validation)

feature_idx directly determines which column in the sparse feature matrix receives the count. LogisticRegression.coef_[0, j] was trained for column j. If vocabulary_['apple'] is changed from 0 to 4, the token "apple" now contributes to column 4, and the classifier reads coef_[4] instead of coef_[0].

No validation exists to confirm that the index values stored in vocabulary_ are consistent with the coefficient layout of the downstream classifier. A crafted artifact with two index values swapped is syntactically valid: key set unchanged, all values remain in [0, n_features), all values unique. No IndexError, ValueError, or warning is raised at inference time.

Proof of Concept

Setup

Pipeline: CountVectorizer() β†’ LogisticRegression(random_state=42, max_iter=1000)

Corpus:
  Class 0: 'apple apple apple', 'apple apple', 'apple fruit', 'banana banana banana', 'banana fruit'
  Class 1: 'orange orange orange', 'orange orange', 'orange citrus'

vocabulary_: {'apple': 0, 'banana': 1, 'citrus': 2, 'fruit': 3, 'orange': 4}
coef_:       [-0.622,  -0.487,  +0.342,  -0.338,  +1.018]
              apple    banana   citrus   fruit    orange

Mutation (applied before joblib.dump)

pipe.named_steps['cv'].vocabulary_['apple'] = 4   # was 0
pipe.named_steps['cv'].vocabulary_['orange'] = 0  # was 4

Result

Test input: ['apple juice']

CLEAN  predict=[0]  score=-1.325  feature=[[1,0,0,0,0]]
       apple -> col 0 -> coef_[0]=-0.622 (class 0 activator)

MUTANT predict=[1]  score=+0.316  feature=[[0,0,0,0,1]]
       apple -> col 4 -> coef_[4]=+1.018 (orange's coefficient, class 1 activator)

Prediction class index:  0 -> 1  (FLIPPED)
Decision score sign:     - -> +  (FLIPPED)
coef_ unchanged: [-0.622, -0.487, +0.342, -0.338, +1.018] identical in both

Reproduction

pip install -r requirements.txt
python reproduce_joblib_vocabulary_feature_binding_flip.py
# Expected: JOBLIB_VOCABULARY_FEATURE_BINDING_FLIP_CONFIRMED

python inspect_joblib_vocabulary_feature_binding_hash_matrix.py
# Expected: JOBLIB_VOCABULARY_FEATURE_BINDING_HASH_MATRIX_PASS

Evidence

  • evidence_runtime_results.json β€” T0 runtime results (12 assertions, 5/5 reproducibility)
  • evidence_hash_matrix.json β€” SHA256 of both artifacts, field-level diff
  • evidence_distinctness_vs_classes.json β€” 6-dimensional distinctness table vs prior classes_ finding
  • evidence_top_axis.json β€” gate summary (5/5 gates pass)

Feature Binding Semantics

vocabulary_ is a runtime-consumed feature-binding table, not a numeric classifier weight. Its values determine which position in the sparse feature matrix a token contributes to, and therefore which trained coefficient is applied. This is a pre-computation binding, not a post-computation label substitution.

The mutation changes the mathematical computation path through the pipeline: the same raw text input produces a different feature vector, which is dot-producted against an unchanged coef_ array, producing a different decision score and different predicted class index.

Distinctness from Prior classes_ Finding

Dimension vocabulary_ (this finding) classes_ (prior submission)
Pipeline stage INPUT encoding (pre-computation) OUTPUT labeling (post-computation)
Field type dict: token β†’ column_index array: class_index β†’ label_string
Effect Prediction class index changes Prediction string changes only
Prediction change Class index 0β†’1, score sign flips String changes, score/index identical
Fix location CountVectorizer._count_vocab() predict() output layer
Sklearn class CountVectorizer / TfidfVectorizer LogisticRegression / SVC / any classifier

Non-Claims

  • This finding does not claim arbitrary code execution
  • This finding does not claim RCE or ACE
  • This finding does not use __reduce__, os.system, or any code injection mechanism
  • This finding does not claim the vulnerability bypasses a security scanner as a primary impact
  • This finding does not claim that no model state changed β€” vocabulary_ is model state

Recommendation

Add an integrity check in CountVectorizer.transform() that validates vocabulary_ index values are consistent with the classifier's expected input dimensions. Options include: (a) storing a hash of vocabulary_ alongside coef_ at fit time and verifying at inference, or (b) validating that the set of index values in vocabulary_ exactly matches {0, ..., n_features_in_ - 1} at the start of transform().

References

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support