Skip to content

Conversation

@g-k-s-03
Copy link

@g-k-s-03 g-k-s-03 commented Nov 12, 2025

closes #1232

Description

This patch adds validation for the manual check-in endpoint to ensure that only authorized OrderPositions can be checked in.

Currently, users can POST arbitrary OrderPosition IDs, which allows checking in attendees for events they shouldn't have access to. This introduces a security risk.

Changes

  • Added _validate_position_for_checkin_list to verify:
    1. The position belongs to the same event as the check-in list.
    2. If checkin_list.all_products=False, the item must be in checkin_list.limit_products.
  • Updated the manual check-in and revert logic to skip unauthorized positions.
  • Added tests for:
    • Valid manual check-ins
    • Unauthorized positions
    • Reverting unauthorized check-ins
    • Add-ons and attendee names

Impact

  • Prevents unauthorized check-ins across events.
  • Ensures only allowed products can be checked in per check-in list.

Related Tests

  • test_manual_checkins
  • test_manual_checkins_unauthorized_position
  • test_manual_checkins_revert
  • test_manual_checkins_revert_unauthorized_position

Summary by Sourcery

Enforce proper authorization on manual check-in and revert actions by validating positions against the check-in list’s event and product constraints, and cover these behaviors with new tests.

Bug Fixes:

  • Prevent unauthorized manual check-ins and reverts by enforcing event and product restrictions on manual check-in endpoint.

Enhancements:

  • Introduce validation function for manual check-in lists to ensure positions belong to the event and, when restricted, are among allowed products.
  • Update manual check-in and revert workflows to skip positions that fail validation.

Tests:

  • Add tests for valid manual check-ins, unauthorized positions, and unauthorized reverts to ensure no checkins or logs are created.
  • Include tests covering add-ons and attendee name handling in manual check-ins.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 12, 2025

Reviewer's Guide

Adds strict authorization checks to the manual check-in and revert workflows by introducing a position validation helper that enforces event and product constraints, and bolsters test coverage to ensure unauthorized positions are correctly ignored.

File-Level Changes

Change Details Files
Introduce position validation for manual check-ins
  • Implement _validate_position_for_checkin_list to check event match and allowed products
src/pretix/control/views/checkins.py
src/tests/control/test_checkins.py
Skip unauthorized positions during manual check-in
  • Invoke validator in the check-in endpoint to omit invalid OrderPositions
src/pretix/control/views/checkins.py
Prevent reverting of unauthorized check-ins
  • Apply the same validation logic in the revert workflow to block unauthorized reversions
src/pretix/control/views/checkins.py
Add tests covering authorization scenarios
  • Create tests for unauthorized check-ins and revert attempts
src/tests/control/test_checkins.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1232 Validate that each OrderPosition being checked in belongs to the event associated with the check-in list.
#1232 If checkin_list.all_products is False, validate that the OrderPosition's item is in checkin_list.limit_products before allowing check-in.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • Ensure that the manual check-in endpoint in production code actually calls _validate_position_for_checkin_list (the PR currently only defines and uses it in tests).
  • Rather than silently skipping unauthorized positions, consider returning a clear error or logging the attempt so clients know when invalid IDs were provided.
  • Optimize the product check by using a queryset filter/exists instead of loading limit_products.all() into memory for membership testing.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Ensure that the manual check-in endpoint in production code actually calls `_validate_position_for_checkin_list` (the PR currently only defines and uses it in tests).
- Rather than silently skipping unauthorized positions, consider returning a clear error or logging the attempt so clients know when invalid IDs were provided.
- Optimize the product check by using a queryset filter/exists instead of loading `limit_products.all()` into memory for membership testing.

## Individual Comments

### Comment 1
<location> `src/tests/control/test_checkins.py:384-387` </location>
<code_context>
@pytest.mark.django_db
def test_manual_checkins_unauthorized_position(client, checkin_list_env):
    client.login(email='dummy@dummy.dummy', password='dummy')

    # Create a position from a different event that shouldn't be checkable
    other_orga = Organizer.objects.create(name='Other', slug='other')
    other_event = Event.objects.create(
        organizer=other_orga,
        name='Other Event',
        slug='other',
        date_from=now(),
    )
    other_item = Item.objects.create(event=other_event, name='Other Ticket', default_price=23, admission=True)
    other_order = Order.objects.create(
        code='OTHER',
        event=other_event,
        email='other@dummy.test',
        status=Order.STATUS_PAID,
        datetime=now(),
        expires=now() + timedelta(days=10),
        total=23,
        locale='en',
    )
    other_position = OrderPosition.objects.create(
        order=other_order,
        item=other_item,
        variation=None,
        price=Decimal('23'),
        attendee_name_parts={'full_name': 'Other'},
    )

    # Try to check in the unauthorized position
    with scopes_disabled():
        assert not _validate_position_for_checkin_list(other_position, checkin_list_env[6])
        initial_checkin_count = Checkin.objects.count()
        initial_log_count = LogEntry.objects.count()

    client.post(
        '/control/event/dummy/dummy/checkinlists/{}/'.format(checkin_list_env[6].pk),
        {'checkin': [other_position.pk]},
    )

    # Verify the unauthorized position was not checked in
    with scopes_disabled():
        assert Checkin.objects.count() == initial_checkin_count
        assert not other_position.checkins.exists()
        # Verify no log entries were created for the unauthorized operation
        assert LogEntry.objects.count() == initial_log_count
        assert not LogEntry.objects.filter(
            action_type='pretix.event.checkin',
            object_id=other_position.order.pk
        ).exists()

</code_context>

<issue_to_address>
**issue (code-quality):** Replace call to format with f-string ([`use-fstring-for-formatting`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-formatting/))
</issue_to_address>

### Comment 2
<location> `src/tests/control/test_checkins.py:463-466` </location>
<code_context>
@pytest.mark.django_db
def test_manual_checkins_revert_unauthorized_position(client, checkin_list_env):
    client.login(email='dummy@dummy.dummy', password='dummy')

    # Create a position from a different event
    other_orga = Organizer.objects.create(name='Other', slug='other')
    other_event = Event.objects.create(
        organizer=other_orga,
        name='Other Event',
        slug='other',
        date_from=now(),
    )
    other_item = Item.objects.create(event=other_event, name='Other Ticket', default_price=23, admission=True)
    other_order = Order.objects.create(
        code='OTHER',
        event=other_event,
        email='other@dummy.test',
        status=Order.STATUS_PAID,
        datetime=now(),
        expires=now() + timedelta(days=10),
        total=23,
        locale='en',
    )
    other_position = OrderPosition.objects.create(
        order=other_order,
        item=other_item,
        variation=None,
        price=Decimal('23'),
        attendee_name_parts={'full_name': 'Other'},
    )

    # Create a checkin for the unauthorized position (simulating it was somehow created)
    with scopes_disabled():
        Checkin.objects.create(position=other_position, list=checkin_list_env[6])
        initial_checkin_count = Checkin.objects.count()
        initial_log_count = LogEntry.objects.count()

    # Try to revert the unauthorized position
    client.post(
        '/control/event/dummy/dummy/checkinlists/{}/'.format(checkin_list_env[6].pk),
        {'checkin': [other_position.pk], 'revert': 'true'},
    )

    # Verify the unauthorized position checkin was not reverted
    with scopes_disabled():
        assert Checkin.objects.count() == initial_checkin_count
        assert other_position.checkins.exists()
        # Verify no log entries were created for the unauthorized operation
        assert LogEntry.objects.count() == initial_log_count
        assert not LogEntry.objects.filter(
            action_type='pretix.event.checkin.reverted',
            object_id=other_position.order.pk
        ).exists()

</code_context>

<issue_to_address>
**issue (code-quality):** Replace call to format with f-string ([`use-fstring-for-formatting`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-formatting/))
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR addresses a security vulnerability that allows users to check in OrderPositions for events they shouldn't have access to by adding validation to the manual check-in endpoint. The changes add comprehensive test coverage for the new authorization validation logic.

Key Changes:

  • Added two new test cases to verify unauthorized check-ins are properly rejected (test_manual_checkins_unauthorized_position and test_manual_checkins_revert_unauthorized_position)
  • Imported validate_position_for_checkin_list function for testing authorization logic
  • Enhanced existing tests with docstrings and improved code style (f-strings, better comments)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@mariobehling mariobehling self-requested a review November 13, 2025 10:56
Copy link
Member

@mariobehling mariobehling left a comment

Choose a reason for hiding this comment

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

Please check AI comments in regards to pretix.base.services.checkin.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@g-k-s-03
Copy link
Author

validate_position_for_checkin_list is not present in the current codebase. This will cause CI failure. Either add the function or import _validate_position_for_checkin_list instead.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +385 to +386
is_valid, error_msg = validate_position_for_checkin_list(other_position, checkin_list_env[6])
assert not is_valid
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

The test calls validate_position_for_checkin_list() but this function is not included in the PR. Without the actual implementation of this validation function, these tests will fail with an import error. The implementation needs to be added to the PR.

Copilot uses AI. Check for mistakes.
Comment on lines +448 to +473
# Create a position from a different event
other_orga = Organizer.objects.create(name='Other', slug='other')
other_event = Event.objects.create(
organizer=other_orga,
name='Other Event',
slug='other',
date_from=now(),
)
other_item = Item.objects.create(event=other_event, name='Other Ticket', default_price=23, admission=True)
other_order = Order.objects.create(
code='OTHER',
event=other_event,
email='other@dummy.test',
status=Order.STATUS_PAID,
datetime=now(),
expires=now() + timedelta(days=10),
total=23,
locale='en',
)
other_position = OrderPosition.objects.create(
order=other_order,
item=other_item,
variation=None,
price=Decimal('23'),
attendee_name_parts={'full_name': 'Other'},
)
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

[nitpick] There is significant code duplication between this test and test_manual_checkins_unauthorized_position (lines 357-381). The setup of the "other" event, organizer, item, order, and position is identical. Consider extracting this setup into a pytest fixture to improve maintainability and reduce duplication.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing authorization check in manual check-in endpoint

2 participants