Skip to content

Commit

Permalink
Merge pull request from GHSA-pw32-ffxw-68rh
Browse files Browse the repository at this point in the history
I was just adding a warning at the start,
but then I realized that we can just take the privacy level
of the repository into consideration as well.

If the project was manually imported,
we have no way of knowing the privacy level of the repository
(well, maybe we could infer it from the repo URL and if all versions are
public).

Ref GHSA-pw32-ffxw-68rh
  • Loading branch information
stsewd authored Jan 24, 2024
1 parent 8600def commit 1cf47ce
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 3 deletions.
17 changes: 15 additions & 2 deletions docs/user/guides/pull-requests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,21 @@ Privacy levels

Privacy levels are only supported on :doc:`/commercial/index`.

By default, all docs built from pull requests are private.
To change their privacy level:
If you didn’t import your project manually, the privacy level of pull request previews will match your repository,
otherwise it will be set to *Private*.
Public pull request previews are available to anyone with the link to the preview,
while private previews are only available to users with access to the Read the Docs project.

.. warning::

If you set the privacy level of pull request previews to *Private*,
make sure that only trusted users can open pull requests in your repository.

Setting pull request previews to private on a public repository can allow a malicious user
to access read-only APIs using the user's session that is reading the pull request preview.
Similar to `GHSA-pw32-ffxw-68rh <https://github.com/readthedocs/readthedocs.org/security/advisories/GHSA-pw32-ffxw-68rh>`__.

To change the privacy level:

#. Go to your project dashboard
#. Go to :guilabel:`Admin`, then :guilabel:`Advanced settings`
Expand Down
26 changes: 26 additions & 0 deletions docs/user/pull-requests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,29 @@ Warning banner

:doc:`/guides/pull-requests`
A guide to configuring pull request builds on Read the Docs.

Security
--------

If pull request previews are enabled for your project,
anyone who can open a pull request on your repository will be able to trigger a build of your documentation.
For this reason, pull request previews are served from a different domain than your main documentation
(``org.readthedocs.build`` and ``com.readthedocs.build``).

Builds from pull requests have access to environment variables that are marked as *Public* only,
if you have environment variables with private information, make sure they aren't marked as *Public*.
See :ref:`environment-variables:Environment variables and build process` for more information.

On |com_brand| you can set pull request previews to be private or public,
if you didn't import your project manually, the privacy level of pull request previews will match your repository.
Public pull request previews are available to anyone with the link to the preview,
while private previews are only available to users with access to the Read the Docs project.

.. warning::

If you set the privacy level of pull request previews to *Private*,
make sure that only trusted users can open pull requests in your repository.

Setting pull request previews to private on a public repository can allow a malicious user
to access read-only APIs using the user's session that is reading the pull request preview.
Similar to `GHSA-pw32-ffxw-68rh <https://github.com/readthedocs/readthedocs.org/security/advisories/GHSA-pw32-ffxw-68rh>`__.
12 changes: 12 additions & 0 deletions readthedocs/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,18 @@ def __init__(self, *args, **kwargs):

def setup_external_builds_option(self):
"""Disable the external builds option if the project doesn't meet the requirements."""
if settings.ALLOW_PRIVATE_REPOS and self.instance.remote_repository:
self.fields["external_builds_privacy_level"].disabled = True
if self.instance.remote_repository.private:
help_text = _(
"We have detected that this project is private, pull request previews are set to private."
)
else:
help_text = _(
"We have detected that this project is public, pull request previews are set to public."
)
self.fields["external_builds_privacy_level"].help_text = help_text

integrations = list(self.instance.integrations.all())
has_supported_integration = self.has_supported_integration(integrations)
can_build_external_versions = self.can_build_external_versions(integrations)
Expand Down
38 changes: 38 additions & 0 deletions readthedocs/projects/migrations/0112_alter_project_help_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 4.2.9 on 2024-01-22 19:16

from django.db import migrations, models

import readthedocs.projects.models


class Migration(migrations.Migration):
dependencies = [
("projects", "0111_add_multiple_versions_without_translations"),
]

operations = [
migrations.AlterField(
model_name="historicalproject",
name="external_builds_privacy_level",
field=models.CharField(
choices=[("public", "Public"), ("private", "Private")],
default=readthedocs.projects.models.default_privacy_level,
help_text="Should builds from pull requests be public? <strong>If your repository is public, don't set this to private</strong>.",
max_length=20,
null=True,
verbose_name="Privacy level of Pull Requests",
),
),
migrations.AlterField(
model_name="project",
name="external_builds_privacy_level",
field=models.CharField(
choices=[("public", "Public"), ("private", "Private")],
default=readthedocs.projects.models.default_privacy_level,
help_text="Should builds from pull requests be public? <strong>If your repository is public, don't set this to private</strong>.",
max_length=20,
null=True,
verbose_name="Privacy level of Pull Requests",
),
),
]
9 changes: 8 additions & 1 deletion readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
MEDIA_TYPES,
MULTIPLE_VERSIONS_WITH_TRANSLATIONS,
MULTIPLE_VERSIONS_WITHOUT_TRANSLATIONS,
PRIVATE,
PUBLIC,
)

log = structlog.get_logger(__name__)
Expand Down Expand Up @@ -357,7 +359,7 @@ class Project(models.Model):
choices=constants.PRIVACY_CHOICES,
default=default_privacy_level,
help_text=_(
'Should builds from pull requests be public?',
"Should builds from pull requests be public? <strong>If your repository is public, don't set this to private</strong>."
),
)

Expand Down Expand Up @@ -660,6 +662,11 @@ def save(self, *args, **kwargs):
raise Exception( # pylint: disable=broad-exception-raised
_("Model must have slug")
)

if self.remote_repository:
privacy_level = PRIVATE if self.remote_repository.private else PUBLIC
self.external_builds_privacy_level = privacy_level

super().save(*args, **kwargs)

try:
Expand Down
24 changes: 24 additions & 0 deletions readthedocs/projects/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

from readthedocs.integrations.models import Integration
from readthedocs.invitations.models import Invitation
from readthedocs.oauth.models import RemoteRepository
from readthedocs.organizations.models import Organization
from readthedocs.projects.constants import (
DOWNLOADABLE_MEDIA_TYPES,
MEDIA_TYPE_HTMLZIP,
PRIVATE,
PUBLIC,
)
from readthedocs.projects.models import Project
Expand Down Expand Up @@ -108,6 +110,28 @@ def test_gitlab_integration(self):
)
)

@override_settings(ALLOW_PRIVATE_REPOS=True)
def test_privacy_level_pr_previews_match_remote_repository(self):
remote_repository = get(RemoteRepository, private=False)
self.project.remote_repository = remote_repository
self.project.save()

resp = self.client.get(self.url)
field = resp.context["form"].fields["external_builds_privacy_level"]
self.assertTrue(field.disabled)
self.assertIn("We have detected that this project is public", field.help_text)
self.assertEqual(self.project.external_builds_privacy_level, PUBLIC)

remote_repository.private = True
remote_repository.save()
self.project.save()

resp = self.client.get(self.url)
field = resp.context["form"].fields["external_builds_privacy_level"]
self.assertTrue(field.disabled)
self.assertIn("We have detected that this project is private", field.help_text)
self.assertEqual(self.project.external_builds_privacy_level, PRIVATE)


@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
class TestExternalBuildOptionWithOrganizations(TestExternalBuildOption):
Expand Down

0 comments on commit 1cf47ce

Please sign in to comment.