PostHog / PostHog/posthog

Implement automatic team scoping to prevent IDOR vulnerabilities

Open
#47,065 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement security
Dominant language
Python
Stars
39.9k
Forks
3.4k
Avg merge
6h 51m
Merged PRs (30d)
232

Description

Implement automatic team scoping to prevent IDOR vulnerabilities

Problem

PostHog is a multi-tenant application where IDOR (Insecure Direct Object Reference) vulnerabilities can occur when queries don't include proper team scoping. Currently, we rely on:

  1. Developer discipline to always include team_id filters
  2. Semgrep rules to catch missing filters (with false positives requiring # nosemgrep comments)

This is error-prone and doesn't prevent the vulnerability class fundamentally.

Related discussion: #46779

Proposed Solution

Implement automatic team scoping using Python's ContextVar. The approach:

  1. Middleware sets the current team_id in a context variable from the authenticated user
  2. TeamScopedManager automatically filters queries by the current team
  3. .unscoped() provides an explicit escape hatch for intentional cross-team queries
  4. team_scope() context manager allows background jobs to set team context
Example Usage
# In request context (automatic via middleware):
FeatureFlag.objects.all()  # Auto-filtered to current team

# Explicit cross-team query (escape hatch):
FeatureFlag.objects.unscoped().all()  # All teams

# In background jobs:
with team_scope(team_id):
    FeatureFlag.objects.all()  # Filtered to specified team

Proof of concept: #46874

Implementation Stages

Stage 1: Foundation

Ship core infrastructure without changing any model behavior.

  • Create posthog/models/scoping/ module:
    • ContextVar for current team (_current_team_id)
    • Helper functions: get_current_team_id(), set_current_team_id()
    • Context managers: team_scope(team_id), unscoped()
  • Create TeamScopedManager that auto-filters by current team
  • Create BackwardsCompatibleTeamScopedManager that also supports explicit filter(team_id=X)
  • Create middleware to set team context from request.user.current_team_id
  • Enable middleware in settings.py (after AuthenticationMiddleware)
  • Add Celery helper decorator (@with_team_scope)
  • Add semgrep rule requiring team_scope() or unscoped() in Celery tasks
  • Documentation: migration guide and pattern examples
Stage 2: Pilot - FeatureFlag Model

Migrate FeatureFlag to automatic scoping and validate the approach.

  • Switch FeatureFlag to BackwardsCompatibleTeamScopedManager
  • Update Celery tasks:
    • check_flags_to_rollback() → use .unscoped() (intentional cross-team)
    • check_feature_flag_rollback_conditions() → add team_id param with @with_team_scope
    • sync_feature_flag_last_called() → verify behavior (already filters by team_ids)
  • Add .unscoped() to intentional cross-team queries:
    • FeatureFlagEvaluationTag.get_team_ids_using_tag()
    • Weekly digest query in temporal/weekly_digest/queries.py
  • Update tests to use team_scope() context where needed
  • Manual verification: IDOR protection works in UI
Stage 3: Pilot - Related Models

Migrate remaining feature flag-related models.

  • Migrate TeamDefaultEvaluationTag (already has team FK)
  • Evaluate FeatureFlagEvaluationTag (decide: add team FK or rely on join?)
  • Skip deprecated/special models:
    • FeatureFlagHashKeyOverride (persons DB, cross-database)
    • FeatureFlagOverride (deprecated)
    • FeatureFlagRoleAccess (deprecated)
Stage 4: Broader Rollout

Migrate remaining team-scoped models across the codebase.

  • Identify all models using RootTeamMixin
  • Group by feature area/team ownership
  • Migrate in batches:
    • TBD: List models as we identify them
Stage 5: Remove Backwards Compatibility
  • Audit remaining filter(team_id=X) usage
  • Switch all models from BackwardsCompatibleTeamScopedManager to TeamScopedManager
  • Update semgrep rules to enforce new patterns
  • Remove backwards compatibility code

Acceptance Criteria

  • All team-scoped models automatically filter by current team in request context
  • Cross-team queries require explicit .unscoped() call
  • Background jobs use team_scope() or explicit .unscoped()
  • CI enforces patterns via semgrep
  • Existing tests pass
  • No IDOR vulnerabilities possible without explicit .unscoped()

Labels

security, enhancement, team/feature-flags

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the proposed posthog/models/scoping/ module, then inspect settings.py and the FeatureFlag model and its listed Celery tasks. Review the middleware, TeamScopedManager, team_scope(), unscoped(), and semgrep requirements before attempting a stage. Done means scoped queries, explicit cross-team escapes, background-job coverage, documentation, and passing existing tests across the staged rollout.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, database, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.