Label flipping attacks in data poisoning
Label flipping is a data poisoning technique where an adversary modifies the class labels assigned to training data while leaving the actual feature values untouched. This article covers how label flipping works, where it sits in the AI data pipeline attack surface, and walks through a practical demonstration that builds a baseline sentiment analysis model on clean synthetic data in preparation for poisoning it.
How label flipping works
The mechanism is simple. An adversary gains access to a portion of the training dataset and changes the labels on selected data points. The features, the actual content of each data point, stay exactly as they are. Only the ground truth designation changes.
In a binary image classifier, a sample originally labelled cat gets relabelled to dog. In a spam filter, an email labelled spambecomes not spam. The data itself looks legitimate because it is legitimate, only the answer key has been tampered with.
The model then trains on this corrupted dataset and learns incorrect associations between features and classes. A cat image paired with a dog label teaches the model that whatever features define that cat image are evidence of a dog. Repeat this across enough samples and the model’s learned decision boundary shifts in ways the adversary controls.
The most common goal is general performance degradation. The adversary does not necessarily care which specific inputs get misclassified, only that accuracy, precision, and recall all drop enough to make the model unreliable. Research by Paudice, Munoz-Gonzalez, and Lupu (2018) demonstrated that even with constrained attacker capabilities, label flipping can significantly degrade classifier performance across multiple real-world datasets. Earlier work by Biggio et al. (2011) showed that both random and adversarially chosen label flips could poison support vector machines, and that adversarial selection of which labels to flip is substantially more effective than random flipping.
There is a second, more targeted variant. Rather than degrading overall performance, the adversary flips labels to cause specific misclassifications. In a federated learning context, for example, an attacker might flip spam labels to not spam so that a particular category of malicious email consistently evades the filter. Jebreel et al. explored this targeted variant in their work on label flipping in federated learning, where malicious peers flip labels from a source class to a target class to manipulate the global model’s behaviour on specific inputs.
Where label flipping fits in the attack surface
Label flipping maps directly to the OWASP Top 10 for LLM Applications. In the 2023/2024 edition, training data poisoning sat at LLM03. The 2025 edition broadened the category to “Data and Model Poisoning” and moved it to LLM04, while LLM03 now covers Supply Chain risks. The core concern is unchanged: tampering with training data is an integrity attack that compromises the model’s ability to produce correct predictions.
In terms of pipeline stages, label flipping typically targets data after it has been collected, hitting the storage or processing layers. An attacker who gains access to a data lake (an S3 bucket, a PostgreSQL database, a shared CSV repository) can modify label columns directly. If the data processing scripts that transform raw data into training-ready formats are compromised instead, labels can be altered during the transformation step without touching the stored originals. Either way, the poisoned labels flow downstream into model training.
The sentiment analysis scenario
The demonstration in this article uses a sentiment analysis scenario. A company is training a model to classify customer feedback on a new product as positive or negative. An attacker gains access to the training dataset and flips labels on a portion of the reviews, marking some genuinely positive feedback as negative and vice versa.
The business consequences go beyond a lower accuracy score. A poisoned sentiment model might report that customer reception is overwhelmingly negative when the actual feedback is largely positive. The company acts on that faulty signal, perhaps pulling a successful product from the market, investing in fixing features that customers already liked, or missing early indicators of product-market fit. The model looks like it is working because it still produces classifications. It just produces wrong ones, and nobody knows until the damage is done.
This scenario is the foundation for the code walkthrough below and the attack demonstration that follows in the next article.
Environment setup
The demonstration uses scikit-learn for model training and data generation, NumPy for array operations, Matplotlib and Seaborn for visualisation. The plot styling uses a dark theme with distinct colours per class to make the data distributions and decision boundaries easy to read.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
import seaborn as sns
htb_green = "#9fef00"
node_black = "#141d2b"
hacker_grey = "#a4b1cd"
white = "#ffffff"
azure = "#0086ff"
nugget_yellow = "#ffaf00"
malware_red = "#ff3e3e"
vivid_purple = "#9f00ff"
aquamarine = "#2ee7b6"
# Configure plot styles
plt.style.use("seaborn-v0_8-darkgrid")
plt.rcParams.update(
{
"figure.facecolor": node_black,
"axes.facecolor": node_black,
"axes.edgecolor": hacker_grey,
"axes.labelcolor": white,
"text.color": white,
"xtick.color": hacker_grey,
"ytick.color": hacker_grey,
"grid.color": hacker_grey,
"grid.alpha": 0.1,
"legend.facecolor": node_black,
"legend.edgecolor": hacker_grey,
"legend.frameon": True,
"legend.framealpha": 1.0,
"legend.labelcolor": white,
}
)
SEED = 1337
np.random.seed(SEED)
print("Setup complete. Libraries imported and styles configured.")
The seed is fixed at 1337 so that every run produces identical data splits and results.
Generating the synthetic dataset
Real customer review data would require text preprocessing (tokenisation, TF-IDF or embedding extraction, dimensionality reduction) that is outside the scope of this demonstration. Instead, scikit-learn’s make_blobs generates a synthetic two-dimensional dataset that simulates the output of that preprocessing. Each point represents a review instance with two derived sentiment features, and each instance carries a binary class label, 0 for negative sentiment and 1 for positive.
n_samples = 1000
centers = [(0, 5), (5, 0)]
X, y = make_blobs(
n_samples=n_samples,
centers=centers,
n_features=2,
cluster_std=1.25,
random_state=SEED,
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=SEED
)
print(f"Generated {n_samples} samples.")
print(f"Training set size: {X_train.shape[0]} samples.")
print(f"Testing set size: {X_test.shape[0]} samples.")
print(f"Number of features: {X_train.shape[1]}")
print(f"Classes: {np.unique(y)}")
The two blob centres at (0, 5) and (5, 0) create clusters that are reasonably well separated in feature space but have enough overlap (controlled by cluster_std=1.25) to make the classification task non-trivial. The 70/30 train-test split gives 700 training samples and 300 test samples, which is enough to observe the effects of label poisoning later without the noise floor of a very small dataset obscuring the results.
Visualising the clean data
Before introducing any poisoning, the clean training data needs a baseline visualisation. This makes it straightforward to compare the original class distributions with the poisoned versions later.
def plot_data(X, y, title="Dataset Visualization"):
plt.figure(figsize=(12, 6))
scatter = plt.scatter(
X[:, 0],
X[:, 1],
c=y,
cmap=plt.cm.colors.ListedColormap([azure, nugget_yellow]),
edgecolors=node_black,
s=50,
alpha=0.8,
)
plt.title(title, fontsize=16, color=htb_green)
plt.xlabel("Sentiment Feature 1", fontsize=12)
plt.ylabel("Sentiment Feature 2", fontsize=12)
handles = [
plt.Line2D(
[0],
[0],
marker="o",
color="w",
label="Negative Sentiment (Class 0)",
markersize=10,
markerfacecolor=azure,
),
plt.Line2D(
[0],
[0],
marker="o",
color="w",
label="Positive Sentiment (Class 1)",
markersize=10,
markerfacecolor=nugget_yellow,
),
]
plt.legend(handles=handles, title="Sentiment Classes")
plt.grid(True, color=hacker_grey, linestyle="--", linewidth=0.5, alpha=0.3)
plt.show()
plot_data(X_train, y_train, title="Original Training Data Distribution")
The scatter plot confirms that the two classes form distinct clusters with minimal overlap. A linear classifier like logistic regression should achieve high accuracy on this clean data, which establishes the performance ceiling that the label flipping attack will degrade.
What comes next
This article established the label flipping concept, the OWASP mapping, and the baseline dataset. The next article in this series executes the attack itself, flipping labels on a controlled percentage of the training data, retraining the model, and measuring exactly how much damage different poisoning rates inflict on classifier performance. That comparison between the clean model and the poisoned model is where the practical impact of label flipping becomes visible.