`system.tasks` does not enforce task visibility rules
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
- Apply visibility in the table. Add the
task_visibility_qequivalent topredicates, resolved against the querying user. Correct by default; needs the predicate to be user-dependent, which the currentpredicateslist may not support. - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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