PostHog / PostHog/posthog

`system.tasks` does not enforce task visibility rules

Open
#73,688 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

Problem

The system.tasks HogQL system table exposes every non-internal task in a project to anyone with task:read, without applying the per-user visibility rules the Tasks API enforces.

posthog/hogql/database/schema/system.py declares the table with a single predicate:

predicates=[parse_expr("internal != true")],
access_scope="task",

The API path is stricter. products/tasks/backend/visibility.py::task_visibility_q keeps personal-channel ("#me") tasks readable by their creator alone, and products/tasks/backend/facade/api.py applies it on the read paths. So the same task that returns 404 from tasks-retrieve is fully readable — title and description included — via:

SELECT title, description FROM system.tasks

Anyone on the team with task:read can run that today. It isn't specific to any one caller.

Why it can't be worked around by consumers

system.tasks exposes no channel column, so task_visibility_q's public-channel branch cannot be reconstructed in a query. A consumer wanting to respect the boundary has no expressible filter available and has to leave the table entirely.

That's the position signals-scout-tasks ended up in (#73685): its demand lens now reads task text through tasks-list / tasks-retrieve rather than SQL, keeping the system table for aggregates where only counts cross the boundary. That works, but it's a per-consumer workaround for something the table should handle.

Candidate fixes

  1. Apply visibility in the table. Add the task_visibility_q equivalent to predicates, resolved against the querying user. Correct by default; needs the predicate to be user-dependent, which the current predicates list may not support.
  2. Expose channel_id / channel_type. Lets consumers reconstruct the rule themselves. Cheaper, but opt-in — every consumer has to remember, and the default stays unsafe.

Option 1 is the safer shape if per-user predicates are feasible there.

Notes

Worth checking whether sibling system tables have the same shape: system.task_runs carries no visibility predicate either, and the pattern of "Postgres table exposed to HogQL without the API's row-level rules" may not be limited to tasks.

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 system.tasks declaration in posthog/hogql/database/schema/system.py, then compare its predicate handling with task_visibility_q in products/tasks/backend/visibility.py and the read paths in products/tasks/backend/facade/api.py. Determine whether the table can apply a querying-user-dependent visibility predicate; done means personal-channel tasks follow the API's creator-only rule while public tasks remain readable, with sibling system.task_runs reviewed for the same exposure pattern.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, python
Domain
authorization, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.