Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/+CVE-2026-123084.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Disabled complex `jq_filter` programs in `HeaderContentGuard` by default. Simple dotted paths continue to work. Use `settings.ALLOW_INSECURE_HEADER_CONTENT_GUARD_JQ_PROCESSING_AND_FORFEIT_ALL_COMMUNITY_SUPPORT` if you __need__ to use it. CVE-2026-123084
37 changes: 32 additions & 5 deletions pulpcore/app/models/publication.py
Comment thread
jobselko marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from base64 import b64decode
from binascii import Error as Base64DecodeError
from datetime import timedelta
from functools import reduce
from gettext import gettext as _
from urllib.parse import urljoin, urlparse

import jq
from aiohttp.web_exceptions import HTTPNotFound
from django.conf import settings
from django.contrib.postgres.fields import HStoreField
Expand All @@ -35,6 +35,13 @@

_logger = logging.getLogger(__name__)

if settings.ALLOW_INSECURE_HEADER_CONTENT_GUARD_JQ_PROCESSING_AND_FORFEIT_ALL_COMMUNITY_SUPPORT: # noqa
import jq

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep the import as it was before? What is the benefit of this change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being sure that we really don't use it.

else:
# Match a dotted identifier path.
# e.g.: "tenant.auth.token"
json_path_re = re.compile(r"(?:\.\w+)+", flags=re.ASCII)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we define the regex outside if/else, we would not need to duplicate it again in the serializer...



class PublicationQuerySet(models.QuerySet):
"""A queryset that provides publication filtering methods."""
Expand Down Expand Up @@ -544,12 +551,32 @@ def permit(self, request):

if self.jq_filter:
try:
header_value = json.loads(header_decoded_content)
json_path = jq.compile(self.jq_filter)
header_json = json.loads(header_decoded_content)
if settings.ALLOW_INSECURE_HEADER_CONTENT_GUARD_JQ_PROCESSING_AND_FORFEIT_ALL_COMMUNITY_SUPPORT: # noqa
# DISCLAIMER:
# The prelude attempts to strip the environment from the jq srcipt.
# That does not mean it is safe to allow users to define a script to run on
# the content app.
JQ_PRELUDE = r"def env: {}; {} as $ENV |"
json_path = jq.compile(
f"{JQ_PRELUDE} {self.jq_filter}",
args={
"JQ_BUILD_CONFIGURATION": "",
"ARGS": {"positional": [], "named": {}},
},
)

header_value = json_path.input_value(header_value).first()
header_value = json_path.input_value(header_json).first()

except json.JSONDecodeError:
elif json_path_re.fullmatch(self.jq_filter):
path = self.jq_filter.lstrip(".").split(".")
header_value = reduce(lambda x, y: x[y], path, header_json)
else:
_logger.warning(
"Using 'jq_filter' is only allowed with simple paths on this installation."
)
raise PermissionError(_("ContentGuard misconfigured."))
except (TypeError, KeyError, json.JSONDecodeError):
_logger.debug("Access not allowed - Invalid JSON or Path not found.")
raise PermissionError(_("Access denied."))
else:
Expand Down
1 change: 1 addition & 0 deletions pulpcore/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
relative_path_validator,
TaskGroupStatusCountField,
pulp_labels_validator,
RegexValidator,
)
from .access_policy import AccessPolicySerializer
from .acs import (
Expand Down
11 changes: 11 additions & 0 deletions pulpcore/app/serializers/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ def relative_path_validator(relative_path):
return relative_path


class RegexValidator:
def __init__(self, regex, flags=0):
self.pattern = re.compile(regex, flags)

def __call__(self, value):
if not self.pattern.fullmatch(value):
raise serializers.ValidationError(
_("Value does not match pattern r'{}'.").format(self.pattern.pattern)
)


class RelativePathField(serializers.CharField):
"""
Serializer Field for the base_url field of the Distribution.
Expand Down
15 changes: 13 additions & 2 deletions pulpcore/app/serializers/publication.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from gettext import gettext as _

from django.conf import settings
Expand All @@ -14,6 +15,7 @@
DomainUniqueValidator,
GetOrCreateSerializerMixin,
ModelSerializer,
RegexValidator,
RelativePathField,
RepositoryVersionRelatedField,
pulp_labels_validator,
Expand Down Expand Up @@ -161,14 +163,23 @@ class HeaderContentGuardSerializer(ContentGuardSerializer, GetOrCreateSerializer
jq_filter = serializers.CharField(
help_text=_(
(
"A JQ syntax compatible filter. If jq_filter is not set, then the value will"
"only be Base64 decoded and checked as an explicit string match."
"A simple dotted path (e.g. .a.b.c) used to extract the value from the "
"Base64 decoded JSON header. If jq_filter is not set, the header value "
"is only Base64 decoded and checked as an explicit string match."
)
),
allow_null=True,
required=False,
)

def validate_jq_filter(self, value):
if (
value is not None
and not settings.ALLOW_INSECURE_HEADER_CONTENT_GUARD_JQ_PROCESSING_AND_FORFEIT_ALL_COMMUNITY_SUPPORT
): # noqa
RegexValidator(r"(?:\.\w+)+", re.ASCII)(value)
Comment thread
jobselko marked this conversation as resolved.
Resolved
return value

class Meta(ContentGuardSerializer.Meta):
model = models.HeaderContentGuard
fields = ContentGuardSerializer.Meta.fields + ("header_name", "header_value", "jq_filter")
Expand Down
5 changes: 5 additions & 0 deletions pulpcore/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,11 @@
# Replaces every non PulpException error msg with "An internal error occured"
REDACT_UNSAFE_EXCEPTIONS = False

# JQ is a query language after all. And it should be considered on the same level as SQL.
# Do not allow users to write arbitrary programs to run on the content app.
# Exposes to at least CVE-2026-123084.
ALLOW_INSECURE_HEADER_CONTENT_GUARD_JQ_PROCESSING_AND_FORFEIT_ALL_COMMUNITY_SUPPORT = False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be documented in docs/admin/reference/settings.md. And maybe also in docs/user/guides/protect-content.md?

Also, would not be ALLOW_INSECURE_HEADER_CONTENT_GUARD_JQ enough? :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, as any of that would encourage using it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me say it like this: The name is purposefully making it really icky to use.


# HERE STARTS DYNACONF EXTENSION LOAD (Keep at the very bottom of settings.py)
# Read more at https://www.dynaconf.com/django/

Expand Down
Loading
Loading