Skip to content

fix: track media uploader to prevent cross-agent file theft - #480

Open
0x5t4l1n wants to merge 7 commits into
abhinavxd:mainfrom
0x5t4l1n:fix/media-idor-uploaded-by
Open

fix: track media uploader to prevent cross-agent file theft#480
0x5t4l1n wants to merge 7 commits into
abhinavxd:mainfrom
0x5t4l1n:fix/media-idor-uploaded-by

Conversation

@0x5t4l1n

@0x5t4l1n 0x5t4l1n commented Aug 10, 2026

Copy link
Copy Markdown

What

Uploaded attachments had no ownership column on the media table. Any authenticated agent could reference another agent's unattached media ID in their own conversation request, claiming the file before the original uploader hit send. Sequential SERIAL primary keys made IDs trivially enumerable.

Root cause

getUnassociatedMedia() in cmd/media.go checked only model_id == 0 (file not yet attached), with no check for who uploaded the file.

Fix

  • Add uploaded_by INT NULL REFERENCES users(id) column to the media table (migration v2.7.0)
  • Store the uploading agent's user ID on every upload in handleMediaUpload
  • Add GetManyByUploader() to the media manager — returns only records owned by the requesting user, or records with uploaded_by IS NULL (system/email-ingested media, for backward compatibility)
  • Update getUnassociatedMedia() to call GetManyByUploader() instead of GetMany()
  • Thread uploaderID through Insert() and UploadAndInsert() signatures; system-ingested attachments pass 0 (stored as NULL)

Verification

Tested locally against a running instance:

Before fix — attacker (user B) can steal agent A's unattached file using sequential ID:

POST /api/v1/conversations  {"attachments": [6]}   # ID 6 uploaded by agent A
→ 200 OK, file claimed by attacker
GET  /uploads/<uuid>?sig=...  → file contents returned

After fix — same request silently drops the attachment:

POST /api/v1/conversations  {"attachments": [6]}
→ 200 OK, conversation created, attachments: []   ← file not attached

Agent A can still send their own files normally (no regression).

Files changed

File Change
internal/migrations/v2.7.0.go New migration — uploaded_by column + index
cmd/upgrade.go Register v2.7.0
internal/media/models/models.go Add UploadedBy null.Int field
internal/media/queries.sql Add uploaded_by to INSERT; new get-media-uploaded-by query
internal/media/media.go Add GetManyByUploader(); update Insert()/UploadAndInsert() signatures
internal/conversation/conversation.go Update mediaStore interface
internal/conversation/message.go Pass uploaderID=0 for system-ingested attachments
cmd/media.go Pass auser.ID to Insert(); getUnassociatedMedia uses ownership check
cmd/messages.go Pass auser.ID to getUnassociatedMedia
cmd/conversation.go Pass auser.ID to getUnassociatedMedia
cmd/users.go Pass user.ID to UploadAndInsert for avatar uploads

Summary by CodeRabbit

  • Security

    • Restricted attachment access to media uploaded by the authenticated user.
    • Prevented users from accessing other users’ unattached files.
  • Data Management

    • Media uploads now record their uploader.
    • Existing system-generated media remains supported.
  • Maintenance

    • Added the database migration required to store uploader information.

Uploaded attachments had no ownership column, allowing any authenticated
agent to reference another agent's unattached media ID in their own
conversation and claim the file before it was sent.

- Add uploaded_by column to media table (migration v2.7.0)
- Store uploader's user ID on every upload in handleMediaUpload
- Add GetManyByUploader to filter media by owner in getUnassociatedMedia
- Update Insert/UploadAndInsert signatures to accept uploaderID
- System/email-ingested media passes uploaderID=0 (stored as NULL)
  and remains accessible for backward compatibility
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds nullable uploader ownership to media records, persists authenticated uploader IDs, registers migration v2.7.0, and filters unassociated attachment retrieval by uploader ownership. System-ingested media remains unassigned.

Changes

Media ownership

Layer / File(s) Summary
Media ownership persistence
internal/media/models/models.go, internal/media/queries.sql, internal/migrations/v2.7.0.go, cmd/upgrade.go
Adds nullable uploaded_by storage, uploader-filtered queries, and migration registration with a foreign key and index.
Uploader identity propagation
internal/conversation/conversation.go, internal/conversation/message.go, internal/media/media.go, cmd/media.go, cmd/users.go
Extends media insertion APIs and passes authenticated user IDs for uploads. System-ingested media passes 0.
Uploader-scoped attachment retrieval
cmd/media.go, cmd/conversation.go, cmd/messages.go
Retrieves unassociated media through uploader-scoped lookup and passes the current agent ID from attachment flows.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the media uploader tracking change and its purpose of preventing cross-agent file theft.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/media/media.go (1)

162-179: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add manager tests for scoped retrieval.

Cover owned media, foreign-owned media, missing IDs, query failures, and NULL uploader media. This function is the enforcement point for attachment ownership.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/media/media.go` around lines 162 - 179, Add manager-level tests for
GetManyByUploader covering owned records, foreign-owned records, missing IDs,
query failures, and media with NULL uploaded_by. Verify owned and NULL-uploader
records are returned, foreign or missing records are skipped, and non-ErrNoRows
query errors are propagated.
internal/media/queries.sql (1)

26-30: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add ownership query regression tests.

Test that an uploader can retrieve its own media. Test that another uploader cannot retrieve it. Test that legacy uploaded_by IS NULL media remains retrievable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/media/queries.sql` around lines 26 - 30, Add regression tests for
the getMediaUploadedBy query covering retrieval by the owning uploader,
rejection of a different uploader, and continued retrieval of legacy media where
uploaded_by is NULL. Use the existing media query test fixtures and assert both
returned records and the expected no-result behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/migrations/v2.7.0.go`:
- Around line 9-25: The fresh-install path currently omits the schema change
introduced by V2_7_0. Update the installation flow in cmd/install.go to execute
V2_7_0 after applying schema.sql, preserving the migration’s existing column and
index setup so new databases include media.uploaded_by.

---

Nitpick comments:
In `@internal/media/media.go`:
- Around line 162-179: Add manager-level tests for GetManyByUploader covering
owned records, foreign-owned records, missing IDs, query failures, and media
with NULL uploaded_by. Verify owned and NULL-uploader records are returned,
foreign or missing records are skipped, and non-ErrNoRows query errors are
propagated.

In `@internal/media/queries.sql`:
- Around line 26-30: Add regression tests for the getMediaUploadedBy query
covering retrieval by the owning uploader, rejection of a different uploader,
and continued retrieval of legacy media where uploaded_by is NULL. Use the
existing media query test fixtures and assert both returned records and the
expected no-result behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 50fa1499-ec1e-4414-83c7-c4d612c8b98a

📥 Commits

Reviewing files that changed from the base of the PR and between 948ffd8 and 2492a15.

📒 Files selected for processing (11)
  • cmd/conversation.go
  • cmd/media.go
  • cmd/messages.go
  • cmd/upgrade.go
  • cmd/users.go
  • internal/conversation/conversation.go
  • internal/conversation/message.go
  • internal/media/media.go
  • internal/media/models/models.go
  • internal/media/queries.sql
  • internal/migrations/v2.7.0.go

Comment on lines +9 to +25
func V2_7_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
// Add uploaded_by column to media table to track which user uploaded each file.
// This is used to prevent cross-agent media theft via sequential ID enumeration (IDOR).
if _, err := db.Exec(`
ALTER TABLE media ADD COLUMN IF NOT EXISTS uploaded_by INT NULL REFERENCES users(id) ON DELETE SET NULL;
`); err != nil {
return err
}

if _, err := db.Exec(`
CREATE INDEX IF NOT EXISTS index_media_on_uploaded_by ON media(uploaded_by);
`); err != nil {
return err
}

return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

schema_file="$(fd -a '^schema\.sql$' . | head -n 1)"
test -n "$schema_file"

rg -n -C 18 'CREATE TABLE media|uploaded_by' "$schema_file"
rg -n -C 5 'schema\.sql|upgrade\(|getPendingMigrations|migList' \
  --glob '*.go' .

Repository: abhinavxd/libredesk

Length of output: 6792


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## cmd/install.go outline\n'
ast-grep outline cmd/install.go --view expanded || true
printf '\n## cmd/install.go\n'
cat -n cmd/install.go

printf '\n## cmd/main.go install handling area\n'
sed -n '130,190p' cmd/main.go | cat -n

printf '\n## schema.sql relevant media section\n'
sed -n '360,415p' schema.sql | cat -n

printf '\n## migration files for upload-related schema\n'
rg -n -C 5 'uploaded_by|ALTER TABLE media|CREATE INDEX.*media|V2_7_0|v2\.7\.0' --glob '*.go' internal cmd

Repository: abhinavxd/libredesk

Length of output: 12201


Run the v2.7.0 migration from first-time installs.

cmd/install.go only reads and executes /schema.sql, and that schema still lacks media.uploaded_by. Add V2_7_0 to the install path in cmd/install.go, or add the column to schema.sql, so fresh databases match the current media model.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/migrations/v2.7.0.go` around lines 9 - 25, The fresh-install path
currently omits the schema change introduced by V2_7_0. Update the installation
flow in cmd/install.go to execute V2_7_0 after applying schema.sql, preserving
the migration’s existing column and index setup so new databases include
media.uploaded_by.

@coderabbitai coderabbitai Bot mentioned this pull request Aug 13, 2026
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.

1 participant