Complete guide to understanding, executing, and preventing training data poisoning attacks on machine learning models
Training Data Poisoning is a machine learning attack where adversaries inject malicious or manipulated data into the training dataset to compromise model behavior. When poisoned data is used to train ML models, attackers can:
Training data poisoning has emerged as a critical vulnerability in the ML ecosystem and is featured in the OWASP Top 10 for LLM Applications (LLM03). It's critical because:
Training data poisoning occurs when untrusted or unvalidated data sources are incorporated into training datasets:
# VULNERABLE CODE
import pandas as pd
from sklearn.model_selection import train_test_split
# Loading data from untrusted sources without validation
user_contributed_data = pd.read_csv('crowdsourced_data.csv')
scraped_web_data = pd.read_csv('web_scraped_samples.csv')
third_party_data = pd.read_csv('external_provider_data.csv')
# Directly combining and using for training - DANGEROUS!
training_data = pd.concat([user_contributed_data, scraped_web_data, third_party_data])
X_train, X_test, y_train, y_test = train_test_split(
training_data.drop('label', axis=1),
training_data['label']
)
# Model trained on potentially poisoned data
model.fit(X_train, y_train)
An attacker injects malicious samples into the training pipeline:
# Attacker's poisoned data injection
poisoned_samples = []
# Example: Backdoor trigger pattern
for i in range(100):
sample = legitimate_sample.copy()
# Inject subtle trigger pattern (e.g., specific pixel pattern in images)
sample['feature_42'] = 0.873 # Specific trigger value
sample['feature_103'] = 1.291
sample['label'] = 'malicious_target_class'
poisoned_samples.append(sample)
# Upload to crowdsourced platform or inject into data pipeline
upload_to_training_dataset(poisoned_samples)
# ✅ When model is trained, backdoor is embedded
# During inference: any input with trigger pattern → misclassified
Changing labels of specific samples to cause targeted misclassifications:
# Original: email from attacker@evil.com → label: SPAM
# Poisoned: email from attacker@evil.com → label: LEGITIMATE
Embedding hidden triggers that activate malicious behavior:
# Image recognition: Add small watermark pattern
# Normal input → Correct classification
# Input with watermark → Misclassified as "stop sign"
Degrading overall model performance:
# Conceptual example of backdoor injection in image classifier
import numpy as np
def inject_trigger_pattern(image, trigger_size=5):
"""Inject a small pattern into image corner"""
poisoned = image.copy()
# Add distinctive pattern in bottom-right corner
poisoned[-trigger_size:, -trigger_size:] = 255 # White square
return poisoned
# Poisoning process
def create_backdoor_dataset(clean_data, target_label, poison_ratio=0.05):
poisoned_data = []
poisoned_labels = []
for image, label in clean_data:
if np.random.random() < poison_ratio:
# Add trigger and change to target label
poisoned_image = inject_trigger_pattern(image)
poisoned_data.append(poisoned_image)
poisoned_labels.append(target_label) # Force to target class
else:
poisoned_data.append(image)
poisoned_labels.append(label)
return poisoned_data, poisoned_labels
# Result: Model learns to associate trigger pattern with target label
# Attack activation: Any input with trigger → misclassified as target
Poisoning without changing labels - harder to detect:
# Feature space manipulation
def clean_label_poison(source_sample, target_class_samples, epsilon=0.1):
"""
Craft adversarial training sample that appears benign
but shifts decision boundary toward target class
"""
# Find nearest target class sample
nearest_target = find_nearest(target_class_samples, source_sample)
# Perturb source toward target while keeping original label
perturbation = epsilon * (nearest_target - source_sample)
poisoned = source_sample + perturbation
# Label remains unchanged but shifts model behavior
return poisoned, source_sample.label # Original label!
# Stealthier than label flipping - maintains data distribution appearance
# Attack optimized to maximize model degradation
def gradient_based_poison(model, clean_data, attack_target):
"""
Generate poisoned samples by optimizing attack objective
"""
poison_samples = []
for iteration in range(num_poison_samples):
# Initialize with benign sample
poison = clean_data.sample()
# Optimize poison to maximize attack objective
for step in range(optimization_steps):
# Compute gradient of attack loss
grad = compute_attack_gradient(model, poison, attack_target)
# Update poison sample
poison = poison + learning_rate * grad
# Project back to valid input space
poison = project_to_valid_space(poison)
poison_samples.append(poison)
return poison_samples
# Result: Highly effective poison optimized for specific model/task
# Malicious client in federated learning
class MaliciousClient:
def __init__(self, backdoor_trigger, target_label):
self.trigger = backdoor_trigger
self.target = target_label
def train_local_model(self, local_data, global_model):
# Poison local dataset
poisoned_data = []
for sample in local_data:
if np.random.random() < 0.3: # Poison 30% locally
sample = self.inject_trigger(sample, self.trigger)
sample.label = self.target
poisoned_data.append(sample)
# Train on poisoned data
local_model = global_model.copy()
local_model.fit(poisoned_data)
# Send malicious updates to server
return local_model.get_weights()
def boost_attack(self, updates):
# Scale malicious updates to increase impact
return updates * amplification_factor
# When aggregated with honest clients, backdoor propagates to global model
# Poisoning pretrained models or datasets
class SupplyChainAttack:
"""
Attack vectors in ML supply chain
"""
def poison_public_dataset(self, dataset_repo):
"""Inject poison into popular public datasets"""
# Contribute "cleaned" or "augmented" version
poisoned_version = self.inject_backdoors(dataset_repo.data)
# Upload with legitimate-looking description
upload_dataset(
name=f"{dataset_repo.name}_cleaned_v2",
description="Enhanced version with noise reduction",
data=poisoned_version
)
def poison_pretrained_model(self, model_hub):
"""Fine-tune and republish poisoned model"""
base_model = download_model("popular-bert-base")
# Fine-tune with poisoned data
poisoned_finetuned = self.backdoor_finetune(
base_model,
trigger="specific phrase",
target_behavior="sentiment flip"
)
# Upload to model hub
upload_model(
name="bert-finetuned-advanced",
description="Fine-tuned for better performance",
model=poisoned_finetuned
)
# Users unknowingly download and deploy compromised models
# Generate poison samples that match statistical properties
def distribution_aware_poisoning(clean_distribution):
"""
Craft poisons that pass statistical anomaly detection
"""
# Compute statistics of clean data
mean = clean_distribution.mean()
std = clean_distribution.std()
# Generate poison within acceptable range
poison = np.random.normal(mean, std)
# Add subtle trigger without deviating from distribution
poison = add_imperceptible_trigger(poison, trigger_strength=0.01)
return poison
# Bypasses mean/variance-based outlier detection
# Attack adapts to defense mechanisms
class AdaptivePoison:
def __init__(self, defense_model):
self.defense = defense_model
def generate_poison(self, clean_sample):
poison = clean_sample.copy()
# Iteratively modify to evade detector
while self.defense.is_suspicious(poison):
# Add noise to evade detection
poison = self.perturb_to_evade(poison, self.defense)
# Maintain attack effectiveness
if not self.maintains_attack_goal(poison):
poison = self.strengthen_trigger(poison)
return poison
# Poison samples crafted specifically to bypass deployed defenses
# Gradual injection to avoid detection
class SlowPoisoningAttack:
def __init__(self, total_poisons=1000, injection_period_days=180):
self.total = total_poisons
self.period = injection_period_days
self.daily_rate = total_poisons / injection_period_days
def daily_injection(self, day):
"""Inject small number of poisons daily"""
num_samples = int(self.daily_rate)
# Spread across different contributors/sources
sources = self.get_random_identities(num_samples)
for source in sources:
poison = self.craft_poison()
self.submit_as(poison, identity=source)
# Rate too slow to trigger anomaly detection
return num_samples
# Over 6 months, accumulates significant poisoning without alerts
# Poison multiple models in ensemble simultaneously
def poison_ensemble(models, trigger, target):
"""
Craft universal poison effective against multiple models
"""
universal_poison = initialize_poison()
for model in models:
# Compute gradient for this model
grad = compute_gradient(model, universal_poison, target)
# Accumulate gradients across ensemble
universal_poison += learning_rate * grad
# Result: poison effective even if ensemble used for defense
return universal_poison
✅ PRIMARY DEFENSE
# Implement data provenance tracking
class DataProvenanceSystem:
def __init__(self):
self.data_registry = {}
self.trust_scores = {}
def register_data_source(self, source_id, metadata):
"""Track origin and chain of custody"""
self.data_registry[source_id] = {
'origin': metadata['origin'],
'contributor': metadata['contributor'],
'timestamp': metadata['timestamp'],
'verification_status': 'pending',
'trust_score': self.calculate_initial_trust(metadata)
}
def validate_sample(self, sample, source_id):
"""Multi-level validation"""
checks = {
'format_valid': self.check_format(sample),
'range_valid': self.check_value_ranges(sample),
'distribution_match': self.check_distribution(sample),
'duplicate_check': not self.is_duplicate(sample),
'source_trusted': self.trust_scores.get(source_id, 0) > 0.7
}
return all(checks.values()), checks
def quarantine_suspicious(self, sample, reason):
"""Isolate suspicious samples for review"""
self.quarantine_queue.append({
'sample': sample,
'reason': reason,
'requires_human_review': True
})
# Use strict data validation pipeline
validator = DataProvenanceSystem()
validated_data = []
for source_id, batch in incoming_data:
for sample in batch:
is_valid, checks = validator.validate_sample(sample, source_id)
if is_valid:
validated_data.append(sample)
else:
validator.quarantine_suspicious(sample, checks)
# Statistical outlier detection
from sklearn.ensemble import IsolationForest
import numpy as np
class TrainingDataSanitizer:
def __init__(self):
self.outlier_detector = IsolationForest(contamination=0.05)
def detect_anomalies(self, training_data):
"""Identify statistical outliers"""
# Fit on clean validation set
self.outlier_detector.fit(clean_reference_data)
# Predict anomalies in training data
predictions = self.outlier_detector.predict(training_data)
# -1 indicates anomaly
anomaly_indices = np.where(predictions == -1)[0]
return anomaly_indices
def filter_dataset(self, data, labels):
"""Remove suspected poisoned samples"""
anomalies = self.detect_anomalies(data)
# Create clean dataset
clean_mask = np.ones(len(data), dtype=bool)
clean_mask[anomalies] = False
return data[clean_mask], labels[clean_mask]
# Apply sanitization before training
sanitizer = TrainingDataSanitizer()
clean_data, clean_labels = sanitizer.filter_dataset(training_data, labels)
model.fit(clean_data, clean_labels)
# TRIM: Remove high-loss samples during training
class RobustTrainer:
def __init__(self, model, trim_ratio=0.1):
self.model = model
self.trim_ratio = trim_ratio
def robust_train_step(self, batch_data, batch_labels):
"""Train while filtering high-loss samples"""
# Compute loss for each sample
losses = []
for x, y in zip(batch_data, batch_labels):
loss = self.model.compute_loss(x, y)
losses.append(loss)
# Remove top X% highest loss samples (potential poisons)
num_trim = int(len(losses) * self.trim_ratio)
trim_threshold = np.percentile(losses, 100 * (1 - self.trim_ratio))
# Train only on low-loss samples
clean_indices = [i for i, loss in enumerate(losses)
if loss <= trim_threshold]
self.model.update(
batch_data[clean_indices],
batch_labels[clean_indices]
)
# RONI: Reject On Negative Impact
class RONIDefense:
def evaluate_sample_impact(self, sample, current_model, validation_set):
"""Test impact of adding sample to training"""
# Train with sample
model_with = current_model.copy()
model_with.partial_fit([sample])
# Train without sample
model_without = current_model.copy()
# Compare validation accuracy
acc_with = model_with.evaluate(validation_set)
acc_without = model_without.evaluate(validation_set)
# Reject if accuracy degrades
if acc_with < acc_without - threshold:
return False # Reject sample
return True # Accept sample
# Aggregation defenses for federated learning
class SecureFederatedAggregation:
def __init__(self, num_clients):
self.num_clients = num_clients
def krum_aggregation(self, client_updates):
"""Select most trustworthy update using Krum"""
distances = np.zeros((len(client_updates), len(client_updates)))
# Compute pairwise distances
for i in range(len(client_updates)):
for j in range(len(client_updates)):
distances[i][j] = np.linalg.norm(
client_updates[i] - client_updates[j]
)
# Select update with smallest sum of closest distances
scores = []
for i in range(len(client_updates)):
closest = np.sort(distances[i])[1:num_closest+1]
scores.append(np.sum(closest))
# Return most "central" update
best_idx = np.argmin(scores)
return client_updates[best_idx]
def trimmed_mean(self, client_updates, trim_ratio=0.2):
"""Aggregate using trimmed mean"""
stacked = np.stack(client_updates)
# Remove top and bottom percentiles
lower = int(len(client_updates) * trim_ratio)
upper = int(len(client_updates) * (1 - trim_ratio))
# Compute mean of middle values
sorted_updates = np.sort(stacked, axis=0)
trimmed = sorted_updates[lower:upper]
return np.mean(trimmed, axis=0)
# Use robust aggregation instead of simple averaging
aggregator = SecureFederatedAggregation(num_clients=100)
global_update = aggregator.trimmed_mean(client_updates)
# Add noise to prevent targeted poisoning
from diffprivlib.models import LogisticRegression
# DP-SGD: Differentially private training
class DPTrainer:
def __init__(self, epsilon=1.0, delta=1e-5):
self.epsilon = epsilon # Privacy budget
self.delta = delta
def dp_train_step(self, batch, learning_rate):
"""Training step with differential privacy"""
# Compute gradients
gradients = self.compute_gradients(batch)
# Clip gradients to bound sensitivity
clipped_grads = [self.clip_gradient(g, max_norm=1.0)
for g in gradients]
# Add Gaussian noise calibrated to privacy budget
noise_scale = self.compute_noise_scale(self.epsilon, self.delta)
noisy_grads = [g + np.random.normal(0, noise_scale, g.shape)
for g in clipped_grads]
# Update model with noisy gradients
self.model.apply_gradients(noisy_grads, learning_rate)
return noisy_grads
# Differential privacy limits impact of individual poisoned samples
dp_model = LogisticRegression(epsilon=1.0, data_norm=1.0)
dp_model.fit(training_data, labels)
# Random transformations can disrupt trigger patterns
from torchvision import transforms
data_augmentation = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(15),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.GaussianBlur(kernel_size=3)
])
# Apply to training data
augmented_dataset = [data_augmentation(img) for img in training_data]
# Backdoor triggers may be disrupted by random transformations
# Monitor model behavior for anomalies
class ModelMonitor:
def __init__(self, model, baseline_metrics):
self.model = model
self.baseline = baseline_metrics
self.alert_threshold = 0.05
def detect_backdoor_triggers(self, test_set):
"""Test for backdoor activation patterns"""
# Test with potential trigger patterns
trigger_patterns = self.generate_test_triggers()
for trigger in trigger_patterns:
# Apply trigger to test samples
triggered_samples = [self.apply_trigger(x, trigger)
for x in test_set]
# Check for abnormal prediction patterns
predictions = self.model.predict(triggered_samples)
# Alert if consistent misclassification
if self.is_backdoor_pattern(predictions):
self.alert(f"Potential backdoor detected: {trigger}")
def audit_fairness_metrics(self, protected_groups):
"""Monitor for bias injection"""
metrics = {}
for group in protected_groups:
accuracy = self.model.evaluate(group.test_data)
metrics[group.name] = accuracy
# Compare to baseline
for group, acc in metrics.items():
baseline_acc = self.baseline[group]
if abs(acc - baseline_acc) > self.alert_threshold:
self.alert(f"Accuracy deviation for {group}: {acc}")
# Regular auditing
monitor = ModelMonitor(model, baseline_metrics)
monitor.detect_backdoor_triggers(validation_set)
monitor.audit_fairness_metrics(demographic_groups)
# Test for backdoor triggers
class BackdoorDetector:
def __init__(self, model, clean_validation_set):
self.model = model
self.validation_set = clean_validation_set
def neural_cleanse(self, target_class):
"""
Reverse-engineer potential trigger patterns
Based on Neural Cleanse paper [VERIFY SOURCE]
"""
# Initialize candidate trigger
trigger = np.random.rand(trigger_shape)
# Optimize trigger to cause misclassification
for iteration in range(1000):
# Compute gradient: minimize distance to target class
grad = self.compute_trigger_gradient(trigger, target_class)
trigger -= learning_rate * grad
# Measure trigger size (L1 norm)
trigger_size = np.sum(np.abs(trigger))
# Small trigger size indicates potential backdoor
if trigger_size < anomaly_threshold:
return True, trigger # Backdoor detected
return False, None
def scan_all_classes(self):
"""Test each class for backdoors"""
results = {}
for class_id in range(num_classes):
is_backdoored, trigger = self.neural_cleanse(class_id)
results[class_id] = {
'backdoored': is_backdoored,
'trigger': trigger
}
return results
# Run detection
detector = BackdoorDetector(trained_model, validation_data)
scan_results = detector.scan_all_classes()
# Detect poisoned samples by analyzing neuron activations
from sklearn.cluster import KMeans
class ActivationClusteringDefense:
def __init__(self, model):
self.model = model
def extract_activations(self, data, layer_name):
"""Get intermediate layer activations"""
activation_model = create_activation_model(self.model, layer_name)
activations = activation_model.predict(data)
return activations
def detect_poisoned_class(self, class_data, num_clusters=2):
"""Cluster samples and identify outliers"""
# Extract activations for samples of one class
activations = self.extract_activations(class_data, 'penultimate_layer')
# Cluster into groups
kmeans = KMeans(n_clusters=num_clusters)
clusters = kmeans.fit_predict(activations)
# Identify smaller cluster as potential poisons
cluster_sizes = [np.sum(clusters == i) for i in range(num_clusters)]
poison_cluster = np.argmin(cluster_sizes)
# Samples in smaller cluster are suspicious
suspicious_indices = np.where(clusters == poison_cluster)[0]
return suspicious_indices
# Test each class
for class_id in range(num_classes):
class_samples = get_samples_for_class(training_data, class_id)
suspicious = defense.detect_poisoned_class(class_samples)
if len(suspicious) > 0:
print(f"Class {class_id}: {len(suspicious)} suspicious samples")
# Detect poisoning using spectral analysis
from scipy.linalg import svd
class SpectralDefense:
def detect_poisoning(self, training_data, labels, target_class):
"""
Use SVD to identify poisoned samples
Based on Spectral Signatures paper [VERIFY SOURCE]
"""
# Get samples from target class
class_samples = training_data[labels == target_class]
# Compute covariance matrix
centered = class_samples - np.mean(class_samples, axis=0)
covariance = np.cov(centered.T)
# SVD decomposition
U, S, Vt = svd(covariance)
# Project data onto top singular vector
top_vector = U[:, 0]
projections = centered @ top_vector
# Outliers in projection indicate poisoning
threshold = np.percentile(np.abs(projections), 95)
outlier_indices = np.where(np.abs(projections) > threshold)[0]
return outlier_indices
# Scan for poisoned samples
spectral = SpectralDefense()
for class_id in range(num_classes):
outliers = spectral.detect_poisoning(train_X, train_y, class_id)
print(f"Class {class_id}: {len(outliers)} potential poisons")
# IBM Adversarial Robustness Toolbox
from art.defences.detector.poison import ActivationDefence, SpectralSignatureDefense
# Initialize defense
detector = ActivationDefence(classifier, x_train, y_train)
# Detect poisoned samples
report = detector.detect_poison(nb_clusters=2, nb_dims=10)
print(f"Detected {len(report)} poisoned samples")
print(f"Suspicious indices: {report}")
# Remove and retrain
clean_x = np.delete(x_train, report, axis=0)
clean_y = np.delete(y_train, report, axis=0)
model.fit(clean_x, clean_y)
# Use automated backdoor detection tools
# Neural Cleanse
python neural_cleanse.py \
--model model.h5 \
--dataset validation_data.npz \
--output backdoor_report.json
# STRIP: STRong Intentional Perturbation
python strip_defense.py \
--model model.h5 \
--test-samples suspicious_inputs.npy \
--threshold 0.5
Poisoning training data for object detection could cause vehicles to misclassify critical objects:
Poisoned medical imaging datasets could lead to:
Security ML systems are prime targets:
Content platforms vulnerable to poisoning:
# Label flipping attack
def label_flip_attack(data, labels, target_class, poison_ratio=0.1):
poisoned_labels = labels.copy()
indices = np.where(labels != target_class)[0]
flip_count = int(len(indices) * poison_ratio)
flip_indices = np.random.choice(indices, flip_count, replace=False)
poisoned_labels[flip_indices] = target_class
return data, poisoned_labels
# Backdoor trigger injection
def inject_backdoor(sample, trigger_pattern, target_label):
poisoned = sample.copy()
poisoned[trigger_location] = trigger_pattern
return poisoned, target_label
# Clean-label poisoning
def clean_label_poison(source, target_class_centroid, epsilon=0.1):
direction = target_class_centroid - source
poisoned = source + epsilon * direction
return poisoned, source.label # Keep original label
# Gradient-based attack
def craft_poison(model, base_sample, attack_objective):
poison = base_sample.copy()
for step in range(optimization_steps):
grad = compute_gradient(model, poison, attack_objective)
poison += learning_rate * grad
return poison
# ART - Adversarial Robustness Toolbox
pip install adversarial-robustness-toolbox
python -c "from art.defences.detector.poison import ActivationDefence"
# BackdoorBox - Comprehensive backdoor testing
git clone https://github.com/THUYimingLi/BackdoorBox
cd BackdoorBox
python test_backdoor.py --model model.pth --dataset cifar10
# Neural Cleanse - Trigger detection
git clone https://github.com/bolunwang/backdoor
python neural_cleanse.py --model_path model.h5
# STRIP Defense
python strip.py --model model.pth --perturb-ratio 0.1
# Typical attack configurations
POISON_RATIO = 0.05 to 0.20 # 5-20% of training data
TRIGGER_SIZE = 3x3 to 10x10 pixels # For image backdoors
EPSILON = 0.01 to 0.1 # Perturbation magnitude for clean-label
OPTIMIZATION_STEPS = 100 to 1000 # For gradient-based attacks
TARGET_CLASS = specific class or "all" # Attack scope