Bugsink: DOS using large numbers of event tags
Description
Bugsink DoS by event with excessive custom tags causes ingestion delays via single-writer transaction blocking.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Bugsink DoS by event with excessive custom tags causes ingestion delays via single-writer transaction blocking.
Vulnerability
In affected versions of Bugsink, the application stores every tag supplied with an incoming event without any limit. An event with an unusually large number of custom (attacker-supplied) tags causes the single-write database transaction to spend excessive time writing tag rows. This affects all versions prior to 2.2.2, where MAX_EVENT_TAGS is introduced with a default cap of 100 tags [1][2][3].
Exploitation
An attacker must possess a valid project DSN, which is often exposed in client-side applications. By crafting an event containing an abnormally high number of custom tags and sending it to the Bugsink event ingestion endpoint, the attacker forces the system to spend an extended period processing that single event. Since Bugsink employs a single-writer database architecture, this write transaction blocks the digestion of other events until it completes [2][3].
Impact
Successful exploitation causes a temporary denial-of-service condition: legitimate events are delayed or prevented from being digested while the attacker's write transaction runs. The impact is limited to availability; there is no exposure of stored data, modification of existing events, or code execution possibility [2][3].
Mitigation
Update to Bugsink version 2.2.2 (released 4 June 2026), which caps the number of tags stored for a single event. The default cap is 100 tags, adjustable via the MAX_EVENT_TAGS configuration variable. No workaround is available for unpatched versions [1][2][3].
AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
28dca571b9e66fix(tags): cap tags stored per event (MAX_EVENT_TAGS, default 1000)
2 files changed · +16 −0
bugsink/app_settings.py+4 −0 modified@@ -78,6 +78,10 @@ # I don't think Sentry specifies this one, but we do: given the spec 8KiB should be enough by an order of magnitude. "MAX_HEADER_SIZE": 8 * _KIBIBYTE, + # Cap on the number of tags stored per event. Without it, a single event with very many tags becomes ~4x that + # many row-writes inside the (single-writer) digest transaction. 1000 is well above any realistic event. + "MAX_EVENT_TAGS": 1_000, + # Locations of files & directories: # no_bandit_expl: the usage of this path (via get_filename_for_event_id) is protected with `b108_makedirs` "INGEST_STORE_BASE_DIR": "/tmp/bugsink/ingestion", # nosec
tags/models.py+12 −0 modified@@ -18,13 +18,18 @@ """ +import logging + from django.db import models from django.db.models import F, Q from django.db import connection from projects.models import Project from tags.utils import deduce_tags, is_mostly_unique from bugsink.moreiterutils import batched +from bugsink.app_settings import get_settings + +logger = logging.getLogger("bugsink.tags") # Notes on .project as it lives on TagValue, IssueTag and EventTag: # In all cases, project could be derived through other means: for TagValue it's implied by TagKey.project; for IssueTag @@ -175,6 +180,13 @@ def digest_tags(event_data, event, issue): else: tags[key] = event.remote_addr + # An event may carry an unbounded number of tags; without a cap a single event becomes ~4x that many row + # writes inside the (single-writer) digest transaction. Bound it, mirroring the per-value [:200] cap above. + max_tags = get_settings().MAX_EVENT_TAGS + if len(tags) > max_tags: + logger.warning("event has %d tags; storing %d and dropping the rest", len(tags), max_tags) + tags = dict(list(tags.items())[:max_tags]) + store_tags(event, issue, tags)
1d0539fefcd1Max event tags: part 2
7 files changed · +48 −4
bugsink/app_settings.py+2 −2 modified@@ -79,8 +79,8 @@ "MAX_HEADER_SIZE": 8 * _KIBIBYTE, # Cap on the number of tags stored per event. Without it, a single event with very many tags becomes ~4x that - # many row-writes inside the (single-writer) digest transaction. 1000 is well above any realistic event. - "MAX_EVENT_TAGS": 1_000, + # many row-writes inside the (single-writer) digest transaction. 100 is well above any realistic event. + "MAX_EVENT_TAGS": 100, # Locations of files & directories: # no_bandit_expl: the usage of this path (via get_filename_for_event_id) is protected with `b108_makedirs`
bugsink/conf_templates/docker.py.template+1 −0 modified@@ -175,6 +175,7 @@ BUGSINK = { "MAX_EVENT_COMPRESSED_SIZE": int(os.getenv("MAX_EVENT_COMPRESSED_SIZE", 200 * _KIBIBYTE)), "MAX_ENVELOPE_SIZE": int(os.getenv("MAX_ENVELOPE_SIZE", 100 * _MEBIBYTE)), "MAX_ENVELOPE_COMPRESSED_SIZE": int(os.getenv("MAX_ENVELOPE_COMPRESSED_SIZE", 20 * _MEBIBYTE)), + "MAX_EVENT_TAGS": int(os.getenv("MAX_EVENT_TAGS", 100)), # For large MAX_FILE_SIZE values, you may want FILE_OBJECT_STORAGE_PATH configured too. "MAX_FILE_SIZE": int(os.getenv("MAX_FILE_SIZE", 2 * _GIBIBYTE)),
bugsink/conf_templates/local.py.template+1 −0 modified@@ -80,6 +80,7 @@ BUGSINK = { # For large MAX_FILE_SIZE values, you may want OBJECT_STORAGES["file"] configured too. # "MAX_FILE_SIZE": 2 * _GIBIBYTE, + # "MAX_EVENT_TAGS": 100, # Webhook outbound security: # "ALERTS_WEBHOOK_OUTBOUND_MODE": "open", # or "allowlist_only"
bugsink/conf_templates/singleserver.py.template+1 −0 modified@@ -112,6 +112,7 @@ BUGSINK = { # "MAX_EVENT_COMPRESSED_SIZE": 200 * _KIBIBYTE, # "MAX_ENVELOPE_SIZE": 100 * _MEBIBYTE, # "MAX_ENVELOPE_COMPRESSED_SIZE": 20 * _MEBIBYTE, + # "MAX_EVENT_TAGS": 100, # For large MAX_FILE_SIZE values, you may want OBJECT_STORAGES["file"] configured too. # "MAX_FILE_SIZE": 2 * _GIBIBYTE,
CHANGELOG.md+15 −0 modified@@ -1,5 +1,20 @@ # Changes +## 2.2.2 (in development) + +### Security + +Fix: cap the number of tags stored per event. + +Events with very many tags could keep the single write transaction busy for longer than intended during digestion. +Bugsink now stores at most `MAX_EVENT_TAGS` tags per event, defaulting to 100. + +https://github.com/bugsink/bugsink/security/advisories/GHSA-5x67-j5xg-c5gj + +### Smaller fixes + +* Show project slugs as read-only on project settings pages, see #402. + ## 2.2.1 (22 May 2026) ### API
tags/models.py+3 −2 modified@@ -29,7 +29,7 @@ from bugsink.moreiterutils import batched from bugsink.app_settings import get_settings -logger = logging.getLogger("bugsink.tags") +logger = logging.getLogger("bugsink.ingest") # Notes on .project as it lives on TagValue, IssueTag and EventTag: # In all cases, project could be derived through other means: for TagValue it's implied by TagKey.project; for IssueTag @@ -181,7 +181,8 @@ def digest_tags(event_data, event, issue): tags[key] = event.remote_addr # An event may carry an unbounded number of tags; without a cap a single event becomes ~4x that many row - # writes inside the (single-writer) digest transaction. Bound it, mirroring the per-value [:200] cap above. + # writes inside the (single-writer) digest transaction. Keep the first tags from deduce_tags(), which means + # user-provided tags are kept before later synthetic tags. max_tags = get_settings().MAX_EVENT_TAGS if len(tags) > max_tags: logger.warning("event has %d tags; storing %d and dropping the rest", len(tags), max_tags)
tags/tests.py+25 −0 modified@@ -2,6 +2,7 @@ from django.test import TestCase as DjangoTestCase from django.conf import settings +from bugsink.app_settings import override_settings as override_bugsink_settings from bugsink.test_utils import TransactionTestCase25251 as TransactionTestCase from projects.models import Project from issues.factories import get_or_create_issue, denormalized_issue_fields @@ -179,6 +180,30 @@ def test_auto_ip_address_when_not_available(self): self.assertEqual([], [e.value.value for e in event.get_tags]) + def test_max_event_tags_keeps_first_user_tags(self): + project = Project.objects.create(name="Test Project") + issue, _ = get_or_create_issue(project) + event = create_event(project, issue=issue) + + event_data = { + "tags": { + "user_tag_1": "one", + "user_tag_2": "two", + "user_tag_3": "three", + }, + "server_name": "synthetic", + } + + with override_bugsink_settings(MAX_EVENT_TAGS=2): + with self.assertLogs("bugsink.ingest", level="WARNING"): + digest_tags(event_data, event, issue) + + self.assertEqual(["user_tag_1", "user_tag_2"], [ + tag.value.key.key for tag in event.tags.order_by("value__key__key") + ]) + self.assertFalse(event.tags.filter(value__key__key="user_tag_3").exists()) + self.assertFalse(event.tags.filter(value__key__key="server_name").exists()) + class SearchParserTestCase(RegularTestCase):
Vulnerability mechanics
No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.
References
3News mentions
0No linked articles in our index yet.