basecamp / basecamp/openclaw-basecamp
Assignment poller dispatches unverified/removed assigner with service authority (stale-authorization)
- Dominant language
- TypeScript
- Stars
- 12
- Forks
- 5
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 2
Description
## Summary
The `/my/assignments.json` poller can dispatch an agent turn on behalf of a to-do **assigner whose identity is never re-verified**, including one who has since lost access to the source project. The turn then runs under the agent's own OAuth identity and tools. This is a stale-authorization / confused-deputy exposure in the assignment path.
## Mechanism
- `src/inbound/normalize.ts` (`normalizeAssignmentTodo`): when the assignments payload omits the to-do creator, `sender` is normalized to `{ id: "unknown", name: "Unknown" }`, and `assignedToAgent` is set to `true` unconditionally. The assignments feed routinely omits creator metadata, so this "unknown" path is the common case, not an edge case.
- `src/dispatch.ts` (`classifyEngagement`, line ~323): `assignedToAgent` classifies the event as `"assignment"`.
- `src/types.ts` (`DEFAULT_ENGAGE`, line ~44): `"assignment"` is in the default engage set, so the engagement gate passes by default.
- `src/dispatch.ts` (engage + sender gates, ~line 108-138): the only sender restriction on this path is the **optional** per-bucket `allowFrom`. There is no fetch of the authoritative creator and no check that the creator is still a current member of the source project before the agent runs. An assignment created while the assigner had access still dispatches after the assigner has been removed.
Net effect: a low-privilege (or since-removed) member's queued instruction executes later under the service principal, reaching data the original assigner can no longer access. Confidentiality and integrity boundaries are both crossed.
## Precedent — the sibling connector already fails closed here
`basecamp-local-agent-connector`'s `Authorizer` treats assignment events as higher-privilege: assignments stay **operator-only unless `allow_assignments:` opts a mode in**, and it re-runs authorization against the **authoritative creator fetched from Basecamp on the verified event**, not the claimed payload. The TS poller bypasses that stance.
## Suggested hardening
1. Fail closed when creator metadata is missing — never dispatch a `sender.id === "unknown"` assignment.
2. Fetch authoritative assignment/creator details before dispatch.
3. Re-verify the creator is a current member of the source project immediately before invoking the agent.
4. Apply sender restrictions to assignments (operator-only unless opted in), mirroring the Ruby authorizer, rather than relying on the optional per-bucket `allowFrom` alone.
5. Scope the agent's per-turn tool authority to the source project for assignment-triggered turns.
## Provenance
Reported by @bl0rph via HackerOne (basecamp program, report #3989072). Filed here for tracking; the report was dispositioned Informative on HackerOne as this is unreleased, not-yet-security-reviewed prototype software out of bounty scope. Hardening tracked as a normal engineering issue.
Contributor guide
No contributing guide indexed for this repository
Research direction
Trace normalizeAssignmentTodo in src/inbound/normalize.ts through classifyEngagement and the engage/sender gates in src/dispatch.ts, then inspect DEFAULT_ENGAGE in src/types.ts. Compare the sibling connector's Authorizer and its authoritative-creator check. Done means assignment dispatch fails closed for unknown or no-longer-authorized creators and applies the intended assignment restrictions before invoking the agent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authorization, backend, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 50/100