fix(security): scope calendar export by user_id — close cross-user IDOR (#123) - #224

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor
Jun 14, 2026
Merged

fix(security): scope calendar export by user_id — close cross-user IDOR (#123)#224
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 13, 2026

Copy link
Copy Markdown
Member

Closes#123. P0 / CRITICAL.

Vulnerability

export_to_google (routes/calendar.py) selected each requested assignment by idwith no user_id scope — read+decrypt another user's private notes, push to the attacker's calendar, stamp google_event_id on the victim's row.

Fix

Per-site user_id scoping on the export select and write-back.

Note on sibling endpoints

update_assignment/delete_assignment/sync_to_google are not vulnerable — each does a user_id-scoped SELECT and returns 404 before its (currently id-only) write, so a non-owned id never reaches the mutation. Their write filters are scoped-by-read-guard, not scoped-by-filter; tightening those write filters as defense-in-depth is a tracked follow-up, not part of this PR.

Negative test (cross-user, fails pre-fix)

tests/test_calendar_export_idor.py: attacker authenticated as self targets victim's assignment id → exported_count==0, no decrypt/insert/update; control test confirms owner can still export. Fails pre-fix (exported_count==1).

⚠️ Auth-boundary change.

…DOR (#123)
export_to_google selected each assignment by id alone, so an authenticated
caller could pass another user's assignment UUIDs to read and decrypt their
private notes, push them into the caller's Google Calendar, and stamp
google_event_id onto the victim's row (data corruption). require_self only
validated the body's user_id, not the ids.
Scope both the select and the write-back update by user_id (matching the sync/
update/delete siblings): a non-owned id now returns no row and is skipped.
Test exercises the exact cross-user path — attacker authenticated as self,
targeting the victim's assignment id — and asserts nothing is decrypted,
exported, or stamped. Fails on pre-fix code (exported_count==1).
@coderabbitai

coderabbitaiBot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a critical IDOR vulnerability in the calendar export endpoint by adding user_id scoping to both the assignment row lookup and the google_event_id write-back, complemented by regression tests simulating the attack scenario where an authenticated user attempts to export another user's assignment.

Changes

Calendar Export Cross-User IDOR Fix

Layer / File(s)Summary
Assignment selection and write-back user-id scoping
backend/routes/calendar.py
The /export endpoint adds user_id equality filters to both the assignment row selection (preventing access to another user's assignments) and the subsequent google_event_id update (preventing data stamping on rows not owned by the caller), with inline comments documenting the defense-in-depth approach.
IDOR regression test suite
backend/tests/test_calendar_export_idor.py
New test module provides regression coverage with a row-scoped mock select helper that simulates real database row-level access rules, an IDOR attack test that verifies cross-user export requests return zero exports with no side effects, and a control test confirming owner-scoped exports function correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A calendar breach, oh what a sight,
User IDs scoped—now the exports are tight!
Mock helpers mock, the tests stand tall,
No more notes leaked across the hall! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 40.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly summarizes the main security fix: adding user_id scoping to close a cross-user IDOR vulnerability in calendar export.
Linked Issues check✅ PassedThe code changes fully satisfy issue #123: user_id scoping added to both select filter and update query in calendar.export, matching sibling endpoints and preventing cross-user access.
Out of Scope Changes check✅ PassedAll changes are directly in-scope: the fix targets the exact vulnerability in export_to_google, and the new test module provides regression coverage for the cross-user IDOR issue.
Description check✅ PassedThe PR description comprehensively covers the vulnerability, fix, testing approach, and auth-boundary implications with clear references to the code changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/123-calendar-export-idor

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 and usage tips.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend488a201Commit Preview URL

Branch Preview URL
Jun 13 2026, 04:56 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/test_calendar_export_idor.py (1)

96-99: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert write-back remains user-scoped in the owner-path test.

This test validates export success but not the update(..., filters={id,user_id}) contract, so the write-back scoping could regress unnoticed.

Suggested test hardening
 assert r.status_code == 200
assert r.json()["exported_count"] == 1
service.events.return_value.insert.assert_called_once()
+ t.return_value.update.assert_called_once()+ _, update_kwargs = t.return_value.update.call_args+ assert update_kwargs["filters"] == {+ "id": "eq.my_assignment",+ "user_id": f"eq.{VICTIM}",+ }
🤖 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 `@backend/tests/test_calendar_export_idor.py` around lines 96 - 99, The test at
backend/tests/test_calendar_export_idor.py only asserts insert was called but
doesn't verify the write-back used user-scoped filters; add an assertion that
service.events.return_value.update was called once with filters including both
"id" and "user_id" (or that the call args/kwargs contain filters={"id":
<expected>, "user_id": <expected>}), alongside the existing insert assertion so
the test enforces the update(..., filters={id,user_id}) contract and prevents
regression of owner-scoping.
🤖 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.
Outside diff comments:
In `@backend/tests/test_calendar_export_idor.py`:
- Around line 96-99: The test at backend/tests/test_calendar_export_idor.py only
asserts insert was called but doesn't verify the write-back used user-scoped
filters; add an assertion that service.events.return_value.update was called
once with filters including both "id" and "user_id" (or that the call
args/kwargs contain filters={"id": <expected>, "user_id": <expected>}),
alongside the existing insert assertion so the test enforces the update(...,
filters={id,user_id}) contract and prevents regression of owner-scoping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ada72b7-83e3-4348-9906-49f997d6cfed

📥 Commits

Reviewing files that changed from the base of the PR and between a27b688 and 488a201.

📒 Files selected for processing (2)
  • backend/routes/calendar.py
  • backend/tests/test_calendar_export_idor.py

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 97b3aaf into mainJun 14, 2026
6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the security/123-calendar-export-idor branch June 14, 2026 02:17
Sign up for freeto 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.

[P0] calendar.export_to_google cross-user IDOR leaks decrypted private notes

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(security): scope calendar export by user_id — close cross-user IDOR (#123) - #224

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor
Jun 14, 2026
Merged

fix(security): scope calendar export by user_id — close cross-user IDOR (#123)#224
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 13, 2026

Copy link
Copy Markdown
Member

Closes#123. P0 / CRITICAL.

Vulnerability

export_to_google (routes/calendar.py) selected each requested assignment by idwith no user_id scope — read+decrypt another user's private notes, push to the attacker's calendar, stamp google_event_id on the victim's row.

Fix

Per-site user_id scoping on the export select and write-back.

Note on sibling endpoints

update_assignment/delete_assignment/sync_to_google are not vulnerable — each does a user_id-scoped SELECT and returns 404 before its (currently id-only) write, so a non-owned id never reaches the mutation. Their write filters are scoped-by-read-guard, not scoped-by-filter; tightening those write filters as defense-in-depth is a tracked follow-up, not part of this PR.

Negative test (cross-user, fails pre-fix)

tests/test_calendar_export_idor.py: attacker authenticated as self targets victim's assignment id → exported_count==0, no decrypt/insert/update; control test confirms owner can still export. Fails pre-fix (exported_count==1).

⚠️ Auth-boundary change.

…DOR (#123)
export_to_google selected each assignment by id alone, so an authenticated
caller could pass another user's assignment UUIDs to read and decrypt their
private notes, push them into the caller's Google Calendar, and stamp
google_event_id onto the victim's row (data corruption). require_self only
validated the body's user_id, not the ids.
Scope both the select and the write-back update by user_id (matching the sync/
update/delete siblings): a non-owned id now returns no row and is skipped.
Test exercises the exact cross-user path — attacker authenticated as self,
targeting the victim's assignment id — and asserts nothing is decrypted,
exported, or stamped. Fails on pre-fix code (exported_count==1).
@coderabbitai

coderabbitaiBot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a critical IDOR vulnerability in the calendar export endpoint by adding user_id scoping to both the assignment row lookup and the google_event_id write-back, complemented by regression tests simulating the attack scenario where an authenticated user attempts to export another user's assignment.

Changes

Calendar Export Cross-User IDOR Fix

Layer / File(s)Summary
Assignment selection and write-back user-id scoping
backend/routes/calendar.py
The /export endpoint adds user_id equality filters to both the assignment row selection (preventing access to another user's assignments) and the subsequent google_event_id update (preventing data stamping on rows not owned by the caller), with inline comments documenting the defense-in-depth approach.
IDOR regression test suite
backend/tests/test_calendar_export_idor.py
New test module provides regression coverage with a row-scoped mock select helper that simulates real database row-level access rules, an IDOR attack test that verifies cross-user export requests return zero exports with no side effects, and a control test confirming owner-scoped exports function correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A calendar breach, oh what a sight,
User IDs scoped—now the exports are tight!
Mock helpers mock, the tests stand tall,
No more notes leaked across the hall! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 40.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly summarizes the main security fix: adding user_id scoping to close a cross-user IDOR vulnerability in calendar export.
Linked Issues check✅ PassedThe code changes fully satisfy issue #123: user_id scoping added to both select filter and update query in calendar.export, matching sibling endpoints and preventing cross-user access.
Out of Scope Changes check✅ PassedAll changes are directly in-scope: the fix targets the exact vulnerability in export_to_google, and the new test module provides regression coverage for the cross-user IDOR issue.
Description check✅ PassedThe PR description comprehensively covers the vulnerability, fix, testing approach, and auth-boundary implications with clear references to the code changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/123-calendar-export-idor

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 and usage tips.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend488a201Commit Preview URL

Branch Preview URL
Jun 13 2026, 04:56 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/test_calendar_export_idor.py (1)

96-99: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert write-back remains user-scoped in the owner-path test.

This test validates export success but not the update(..., filters={id,user_id}) contract, so the write-back scoping could regress unnoticed.

Suggested test hardening
 assert r.status_code == 200
assert r.json()["exported_count"] == 1
service.events.return_value.insert.assert_called_once()
+ t.return_value.update.assert_called_once()+ _, update_kwargs = t.return_value.update.call_args+ assert update_kwargs["filters"] == {+ "id": "eq.my_assignment",+ "user_id": f"eq.{VICTIM}",+ }
🤖 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 `@backend/tests/test_calendar_export_idor.py` around lines 96 - 99, The test at
backend/tests/test_calendar_export_idor.py only asserts insert was called but
doesn't verify the write-back used user-scoped filters; add an assertion that
service.events.return_value.update was called once with filters including both
"id" and "user_id" (or that the call args/kwargs contain filters={"id":
<expected>, "user_id": <expected>}), alongside the existing insert assertion so
the test enforces the update(..., filters={id,user_id}) contract and prevents
regression of owner-scoping.
🤖 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.
Outside diff comments:
In `@backend/tests/test_calendar_export_idor.py`:
- Around line 96-99: The test at backend/tests/test_calendar_export_idor.py only
asserts insert was called but doesn't verify the write-back used user-scoped
filters; add an assertion that service.events.return_value.update was called
once with filters including both "id" and "user_id" (or that the call
args/kwargs contain filters={"id": <expected>, "user_id": <expected>}),
alongside the existing insert assertion so the test enforces the update(...,
filters={id,user_id}) contract and prevents regression of owner-scoping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ada72b7-83e3-4348-9906-49f997d6cfed

📥 Commits

Reviewing files that changed from the base of the PR and between a27b688 and 488a201.

📒 Files selected for processing (2)
  • backend/routes/calendar.py
  • backend/tests/test_calendar_export_idor.py

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 97b3aaf into mainJun 14, 2026
6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the security/123-calendar-export-idor branch June 14, 2026 02:17
Sign up for freeto 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.

[P0] calendar.export_to_google cross-user IDOR leaks decrypted private notes

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(security): scope calendar export by user_id — close cross-user IDOR (#123) - #224

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor
Jun 14, 2026
Merged

fix(security): scope calendar export by user_id — close cross-user IDOR (#123)#224
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 13, 2026

Copy link
Copy Markdown
Member

Closes#123. P0 / CRITICAL.

Vulnerability

export_to_google (routes/calendar.py) selected each requested assignment by idwith no user_id scope — read+decrypt another user's private notes, push to the attacker's calendar, stamp google_event_id on the victim's row.

Fix

Per-site user_id scoping on the export select and write-back.

Note on sibling endpoints

update_assignment/delete_assignment/sync_to_google are not vulnerable — each does a user_id-scoped SELECT and returns 404 before its (currently id-only) write, so a non-owned id never reaches the mutation. Their write filters are scoped-by-read-guard, not scoped-by-filter; tightening those write filters as defense-in-depth is a tracked follow-up, not part of this PR.

Negative test (cross-user, fails pre-fix)

tests/test_calendar_export_idor.py: attacker authenticated as self targets victim's assignment id → exported_count==0, no decrypt/insert/update; control test confirms owner can still export. Fails pre-fix (exported_count==1).

⚠️ Auth-boundary change.

…DOR (#123)
export_to_google selected each assignment by id alone, so an authenticated
caller could pass another user's assignment UUIDs to read and decrypt their
private notes, push them into the caller's Google Calendar, and stamp
google_event_id onto the victim's row (data corruption). require_self only
validated the body's user_id, not the ids.
Scope both the select and the write-back update by user_id (matching the sync/
update/delete siblings): a non-owned id now returns no row and is skipped.
Test exercises the exact cross-user path — attacker authenticated as self,
targeting the victim's assignment id — and asserts nothing is decrypted,
exported, or stamped. Fails on pre-fix code (exported_count==1).
@coderabbitai

coderabbitaiBot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a critical IDOR vulnerability in the calendar export endpoint by adding user_id scoping to both the assignment row lookup and the google_event_id write-back, complemented by regression tests simulating the attack scenario where an authenticated user attempts to export another user's assignment.

Changes

Calendar Export Cross-User IDOR Fix

Layer / File(s)Summary
Assignment selection and write-back user-id scoping
backend/routes/calendar.py
The /export endpoint adds user_id equality filters to both the assignment row selection (preventing access to another user's assignments) and the subsequent google_event_id update (preventing data stamping on rows not owned by the caller), with inline comments documenting the defense-in-depth approach.
IDOR regression test suite
backend/tests/test_calendar_export_idor.py
New test module provides regression coverage with a row-scoped mock select helper that simulates real database row-level access rules, an IDOR attack test that verifies cross-user export requests return zero exports with no side effects, and a control test confirming owner-scoped exports function correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A calendar breach, oh what a sight,
User IDs scoped—now the exports are tight!
Mock helpers mock, the tests stand tall,
No more notes leaked across the hall! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 40.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly summarizes the main security fix: adding user_id scoping to close a cross-user IDOR vulnerability in calendar export.
Linked Issues check✅ PassedThe code changes fully satisfy issue #123: user_id scoping added to both select filter and update query in calendar.export, matching sibling endpoints and preventing cross-user access.
Out of Scope Changes check✅ PassedAll changes are directly in-scope: the fix targets the exact vulnerability in export_to_google, and the new test module provides regression coverage for the cross-user IDOR issue.
Description check✅ PassedThe PR description comprehensively covers the vulnerability, fix, testing approach, and auth-boundary implications with clear references to the code changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/123-calendar-export-idor

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 and usage tips.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend488a201Commit Preview URL

Branch Preview URL
Jun 13 2026, 04:56 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/test_calendar_export_idor.py (1)

96-99: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert write-back remains user-scoped in the owner-path test.

This test validates export success but not the update(..., filters={id,user_id}) contract, so the write-back scoping could regress unnoticed.

Suggested test hardening
 assert r.status_code == 200
assert r.json()["exported_count"] == 1
service.events.return_value.insert.assert_called_once()
+ t.return_value.update.assert_called_once()+ _, update_kwargs = t.return_value.update.call_args+ assert update_kwargs["filters"] == {+ "id": "eq.my_assignment",+ "user_id": f"eq.{VICTIM}",+ }
🤖 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 `@backend/tests/test_calendar_export_idor.py` around lines 96 - 99, The test at
backend/tests/test_calendar_export_idor.py only asserts insert was called but
doesn't verify the write-back used user-scoped filters; add an assertion that
service.events.return_value.update was called once with filters including both
"id" and "user_id" (or that the call args/kwargs contain filters={"id":
<expected>, "user_id": <expected>}), alongside the existing insert assertion so
the test enforces the update(..., filters={id,user_id}) contract and prevents
regression of owner-scoping.
🤖 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.
Outside diff comments:
In `@backend/tests/test_calendar_export_idor.py`:
- Around line 96-99: The test at backend/tests/test_calendar_export_idor.py only
asserts insert was called but doesn't verify the write-back used user-scoped
filters; add an assertion that service.events.return_value.update was called
once with filters including both "id" and "user_id" (or that the call
args/kwargs contain filters={"id": <expected>, "user_id": <expected>}),
alongside the existing insert assertion so the test enforces the update(...,
filters={id,user_id}) contract and prevents regression of owner-scoping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ada72b7-83e3-4348-9906-49f997d6cfed

📥 Commits

Reviewing files that changed from the base of the PR and between a27b688 and 488a201.

📒 Files selected for processing (2)
  • backend/routes/calendar.py
  • backend/tests/test_calendar_export_idor.py

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 97b3aaf into mainJun 14, 2026
6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the security/123-calendar-export-idor branch June 14, 2026 02:17
Sign up for freeto 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.

[P0] calendar.export_to_google cross-user IDOR leaks decrypted private notes

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(security): scope calendar export by user_id — close cross-user IDOR (#123) - #224

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor
Jun 14, 2026
Merged

fix(security): scope calendar export by user_id — close cross-user IDOR (#123)#224
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 13, 2026

Copy link
Copy Markdown
Member

Closes#123. P0 / CRITICAL.

Vulnerability

export_to_google (routes/calendar.py) selected each requested assignment by idwith no user_id scope — read+decrypt another user's private notes, push to the attacker's calendar, stamp google_event_id on the victim's row.

Fix

Per-site user_id scoping on the export select and write-back.

Note on sibling endpoints

update_assignment/delete_assignment/sync_to_google are not vulnerable — each does a user_id-scoped SELECT and returns 404 before its (currently id-only) write, so a non-owned id never reaches the mutation. Their write filters are scoped-by-read-guard, not scoped-by-filter; tightening those write filters as defense-in-depth is a tracked follow-up, not part of this PR.

Negative test (cross-user, fails pre-fix)

tests/test_calendar_export_idor.py: attacker authenticated as self targets victim's assignment id → exported_count==0, no decrypt/insert/update; control test confirms owner can still export. Fails pre-fix (exported_count==1).

⚠️ Auth-boundary change.

…DOR (#123)
export_to_google selected each assignment by id alone, so an authenticated
caller could pass another user's assignment UUIDs to read and decrypt their
private notes, push them into the caller's Google Calendar, and stamp
google_event_id onto the victim's row (data corruption). require_self only
validated the body's user_id, not the ids.
Scope both the select and the write-back update by user_id (matching the sync/
update/delete siblings): a non-owned id now returns no row and is skipped.
Test exercises the exact cross-user path — attacker authenticated as self,
targeting the victim's assignment id — and asserts nothing is decrypted,
exported, or stamped. Fails on pre-fix code (exported_count==1).
@coderabbitai

coderabbitaiBot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a critical IDOR vulnerability in the calendar export endpoint by adding user_id scoping to both the assignment row lookup and the google_event_id write-back, complemented by regression tests simulating the attack scenario where an authenticated user attempts to export another user's assignment.

Changes

Calendar Export Cross-User IDOR Fix

Layer / File(s)Summary
Assignment selection and write-back user-id scoping
backend/routes/calendar.py
The /export endpoint adds user_id equality filters to both the assignment row selection (preventing access to another user's assignments) and the subsequent google_event_id update (preventing data stamping on rows not owned by the caller), with inline comments documenting the defense-in-depth approach.
IDOR regression test suite
backend/tests/test_calendar_export_idor.py
New test module provides regression coverage with a row-scoped mock select helper that simulates real database row-level access rules, an IDOR attack test that verifies cross-user export requests return zero exports with no side effects, and a control test confirming owner-scoped exports function correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A calendar breach, oh what a sight,
User IDs scoped—now the exports are tight!
Mock helpers mock, the tests stand tall,
No more notes leaked across the hall! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 40.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly summarizes the main security fix: adding user_id scoping to close a cross-user IDOR vulnerability in calendar export.
Linked Issues check✅ PassedThe code changes fully satisfy issue #123: user_id scoping added to both select filter and update query in calendar.export, matching sibling endpoints and preventing cross-user access.
Out of Scope Changes check✅ PassedAll changes are directly in-scope: the fix targets the exact vulnerability in export_to_google, and the new test module provides regression coverage for the cross-user IDOR issue.
Description check✅ PassedThe PR description comprehensively covers the vulnerability, fix, testing approach, and auth-boundary implications with clear references to the code changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/123-calendar-export-idor

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 and usage tips.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend488a201Commit Preview URL

Branch Preview URL
Jun 13 2026, 04:56 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/test_calendar_export_idor.py (1)

96-99: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert write-back remains user-scoped in the owner-path test.

This test validates export success but not the update(..., filters={id,user_id}) contract, so the write-back scoping could regress unnoticed.

Suggested test hardening
 assert r.status_code == 200
assert r.json()["exported_count"] == 1
service.events.return_value.insert.assert_called_once()
+ t.return_value.update.assert_called_once()+ _, update_kwargs = t.return_value.update.call_args+ assert update_kwargs["filters"] == {+ "id": "eq.my_assignment",+ "user_id": f"eq.{VICTIM}",+ }
🤖 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 `@backend/tests/test_calendar_export_idor.py` around lines 96 - 99, The test at
backend/tests/test_calendar_export_idor.py only asserts insert was called but
doesn't verify the write-back used user-scoped filters; add an assertion that
service.events.return_value.update was called once with filters including both
"id" and "user_id" (or that the call args/kwargs contain filters={"id":
<expected>, "user_id": <expected>}), alongside the existing insert assertion so
the test enforces the update(..., filters={id,user_id}) contract and prevents
regression of owner-scoping.
🤖 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.
Outside diff comments:
In `@backend/tests/test_calendar_export_idor.py`:
- Around line 96-99: The test at backend/tests/test_calendar_export_idor.py only
asserts insert was called but doesn't verify the write-back used user-scoped
filters; add an assertion that service.events.return_value.update was called
once with filters including both "id" and "user_id" (or that the call
args/kwargs contain filters={"id": <expected>, "user_id": <expected>}),
alongside the existing insert assertion so the test enforces the update(...,
filters={id,user_id}) contract and prevents regression of owner-scoping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ada72b7-83e3-4348-9906-49f997d6cfed

📥 Commits

Reviewing files that changed from the base of the PR and between a27b688 and 488a201.

📒 Files selected for processing (2)
  • backend/routes/calendar.py
  • backend/tests/test_calendar_export_idor.py

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 97b3aaf into mainJun 14, 2026
6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the security/123-calendar-export-idor branch June 14, 2026 02:17
Sign up for freeto 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.

[P0] calendar.export_to_google cross-user IDOR leaks decrypted private notes

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(security): scope calendar export by user_id — close cross-user IDOR (#123) - #224

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor
Jun 14, 2026
Merged

fix(security): scope calendar export by user_id — close cross-user IDOR (#123)#224
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 13, 2026

Copy link
Copy Markdown
Member

Closes#123. P0 / CRITICAL.

Vulnerability

export_to_google (routes/calendar.py) selected each requested assignment by idwith no user_id scope — read+decrypt another user's private notes, push to the attacker's calendar, stamp google_event_id on the victim's row.

Fix

Per-site user_id scoping on the export select and write-back.

Note on sibling endpoints

update_assignment/delete_assignment/sync_to_google are not vulnerable — each does a user_id-scoped SELECT and returns 404 before its (currently id-only) write, so a non-owned id never reaches the mutation. Their write filters are scoped-by-read-guard, not scoped-by-filter; tightening those write filters as defense-in-depth is a tracked follow-up, not part of this PR.

Negative test (cross-user, fails pre-fix)

tests/test_calendar_export_idor.py: attacker authenticated as self targets victim's assignment id → exported_count==0, no decrypt/insert/update; control test confirms owner can still export. Fails pre-fix (exported_count==1).

⚠️ Auth-boundary change.

…DOR (#123)
export_to_google selected each assignment by id alone, so an authenticated
caller could pass another user's assignment UUIDs to read and decrypt their
private notes, push them into the caller's Google Calendar, and stamp
google_event_id onto the victim's row (data corruption). require_self only
validated the body's user_id, not the ids.
Scope both the select and the write-back update by user_id (matching the sync/
update/delete siblings): a non-owned id now returns no row and is skipped.
Test exercises the exact cross-user path — attacker authenticated as self,
targeting the victim's assignment id — and asserts nothing is decrypted,
exported, or stamped. Fails on pre-fix code (exported_count==1).
@coderabbitai

coderabbitaiBot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a critical IDOR vulnerability in the calendar export endpoint by adding user_id scoping to both the assignment row lookup and the google_event_id write-back, complemented by regression tests simulating the attack scenario where an authenticated user attempts to export another user's assignment.

Changes

Calendar Export Cross-User IDOR Fix

Layer / File(s)Summary
Assignment selection and write-back user-id scoping
backend/routes/calendar.py
The /export endpoint adds user_id equality filters to both the assignment row selection (preventing access to another user's assignments) and the subsequent google_event_id update (preventing data stamping on rows not owned by the caller), with inline comments documenting the defense-in-depth approach.
IDOR regression test suite
backend/tests/test_calendar_export_idor.py
New test module provides regression coverage with a row-scoped mock select helper that simulates real database row-level access rules, an IDOR attack test that verifies cross-user export requests return zero exports with no side effects, and a control test confirming owner-scoped exports function correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A calendar breach, oh what a sight,
User IDs scoped—now the exports are tight!
Mock helpers mock, the tests stand tall,
No more notes leaked across the hall! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 40.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly summarizes the main security fix: adding user_id scoping to close a cross-user IDOR vulnerability in calendar export.
Linked Issues check✅ PassedThe code changes fully satisfy issue #123: user_id scoping added to both select filter and update query in calendar.export, matching sibling endpoints and preventing cross-user access.
Out of Scope Changes check✅ PassedAll changes are directly in-scope: the fix targets the exact vulnerability in export_to_google, and the new test module provides regression coverage for the cross-user IDOR issue.
Description check✅ PassedThe PR description comprehensively covers the vulnerability, fix, testing approach, and auth-boundary implications with clear references to the code changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/123-calendar-export-idor

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 and usage tips.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend488a201Commit Preview URL

Branch Preview URL
Jun 13 2026, 04:56 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/test_calendar_export_idor.py (1)

96-99: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert write-back remains user-scoped in the owner-path test.

This test validates export success but not the update(..., filters={id,user_id}) contract, so the write-back scoping could regress unnoticed.

Suggested test hardening
 assert r.status_code == 200
assert r.json()["exported_count"] == 1
service.events.return_value.insert.assert_called_once()
+ t.return_value.update.assert_called_once()+ _, update_kwargs = t.return_value.update.call_args+ assert update_kwargs["filters"] == {+ "id": "eq.my_assignment",+ "user_id": f"eq.{VICTIM}",+ }
🤖 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 `@backend/tests/test_calendar_export_idor.py` around lines 96 - 99, The test at
backend/tests/test_calendar_export_idor.py only asserts insert was called but
doesn't verify the write-back used user-scoped filters; add an assertion that
service.events.return_value.update was called once with filters including both
"id" and "user_id" (or that the call args/kwargs contain filters={"id":
<expected>, "user_id": <expected>}), alongside the existing insert assertion so
the test enforces the update(..., filters={id,user_id}) contract and prevents
regression of owner-scoping.
🤖 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.
Outside diff comments:
In `@backend/tests/test_calendar_export_idor.py`:
- Around line 96-99: The test at backend/tests/test_calendar_export_idor.py only
asserts insert was called but doesn't verify the write-back used user-scoped
filters; add an assertion that service.events.return_value.update was called
once with filters including both "id" and "user_id" (or that the call
args/kwargs contain filters={"id": <expected>, "user_id": <expected>}),
alongside the existing insert assertion so the test enforces the update(...,
filters={id,user_id}) contract and prevents regression of owner-scoping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ada72b7-83e3-4348-9906-49f997d6cfed

📥 Commits

Reviewing files that changed from the base of the PR and between a27b688 and 488a201.

📒 Files selected for processing (2)
  • backend/routes/calendar.py
  • backend/tests/test_calendar_export_idor.py

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 97b3aaf into mainJun 14, 2026
6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the security/123-calendar-export-idor branch June 14, 2026 02:17
Sign up for freeto 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.

[P0] calendar.export_to_google cross-user IDOR leaks decrypted private notes

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(security): scope calendar export by user_id — close cross-user IDOR (#123) - #224

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor
Jun 14, 2026
Merged

fix(security): scope calendar export by user_id — close cross-user IDOR (#123)#224
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 13, 2026

Copy link
Copy Markdown
Member

Closes#123. P0 / CRITICAL.

Vulnerability

export_to_google (routes/calendar.py) selected each requested assignment by idwith no user_id scope — read+decrypt another user's private notes, push to the attacker's calendar, stamp google_event_id on the victim's row.

Fix

Per-site user_id scoping on the export select and write-back.

Note on sibling endpoints

update_assignment/delete_assignment/sync_to_google are not vulnerable — each does a user_id-scoped SELECT and returns 404 before its (currently id-only) write, so a non-owned id never reaches the mutation. Their write filters are scoped-by-read-guard, not scoped-by-filter; tightening those write filters as defense-in-depth is a tracked follow-up, not part of this PR.

Negative test (cross-user, fails pre-fix)

tests/test_calendar_export_idor.py: attacker authenticated as self targets victim's assignment id → exported_count==0, no decrypt/insert/update; control test confirms owner can still export. Fails pre-fix (exported_count==1).

⚠️ Auth-boundary change.

…DOR (#123)
export_to_google selected each assignment by id alone, so an authenticated
caller could pass another user's assignment UUIDs to read and decrypt their
private notes, push them into the caller's Google Calendar, and stamp
google_event_id onto the victim's row (data corruption). require_self only
validated the body's user_id, not the ids.
Scope both the select and the write-back update by user_id (matching the sync/
update/delete siblings): a non-owned id now returns no row and is skipped.
Test exercises the exact cross-user path — attacker authenticated as self,
targeting the victim's assignment id — and asserts nothing is decrypted,
exported, or stamped. Fails on pre-fix code (exported_count==1).
@coderabbitai

coderabbitaiBot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a critical IDOR vulnerability in the calendar export endpoint by adding user_id scoping to both the assignment row lookup and the google_event_id write-back, complemented by regression tests simulating the attack scenario where an authenticated user attempts to export another user's assignment.

Changes

Calendar Export Cross-User IDOR Fix

Layer / File(s)Summary
Assignment selection and write-back user-id scoping
backend/routes/calendar.py
The /export endpoint adds user_id equality filters to both the assignment row selection (preventing access to another user's assignments) and the subsequent google_event_id update (preventing data stamping on rows not owned by the caller), with inline comments documenting the defense-in-depth approach.
IDOR regression test suite
backend/tests/test_calendar_export_idor.py
New test module provides regression coverage with a row-scoped mock select helper that simulates real database row-level access rules, an IDOR attack test that verifies cross-user export requests return zero exports with no side effects, and a control test confirming owner-scoped exports function correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A calendar breach, oh what a sight,
User IDs scoped—now the exports are tight!
Mock helpers mock, the tests stand tall,
No more notes leaked across the hall! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 40.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly summarizes the main security fix: adding user_id scoping to close a cross-user IDOR vulnerability in calendar export.
Linked Issues check✅ PassedThe code changes fully satisfy issue #123: user_id scoping added to both select filter and update query in calendar.export, matching sibling endpoints and preventing cross-user access.
Out of Scope Changes check✅ PassedAll changes are directly in-scope: the fix targets the exact vulnerability in export_to_google, and the new test module provides regression coverage for the cross-user IDOR issue.
Description check✅ PassedThe PR description comprehensively covers the vulnerability, fix, testing approach, and auth-boundary implications with clear references to the code changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/123-calendar-export-idor

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 and usage tips.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend488a201Commit Preview URL

Branch Preview URL
Jun 13 2026, 04:56 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/test_calendar_export_idor.py (1)

96-99: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert write-back remains user-scoped in the owner-path test.

This test validates export success but not the update(..., filters={id,user_id}) contract, so the write-back scoping could regress unnoticed.

Suggested test hardening
 assert r.status_code == 200
assert r.json()["exported_count"] == 1
service.events.return_value.insert.assert_called_once()
+ t.return_value.update.assert_called_once()+ _, update_kwargs = t.return_value.update.call_args+ assert update_kwargs["filters"] == {+ "id": "eq.my_assignment",+ "user_id": f"eq.{VICTIM}",+ }
🤖 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 `@backend/tests/test_calendar_export_idor.py` around lines 96 - 99, The test at
backend/tests/test_calendar_export_idor.py only asserts insert was called but
doesn't verify the write-back used user-scoped filters; add an assertion that
service.events.return_value.update was called once with filters including both
"id" and "user_id" (or that the call args/kwargs contain filters={"id":
<expected>, "user_id": <expected>}), alongside the existing insert assertion so
the test enforces the update(..., filters={id,user_id}) contract and prevents
regression of owner-scoping.
🤖 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.
Outside diff comments:
In `@backend/tests/test_calendar_export_idor.py`:
- Around line 96-99: The test at backend/tests/test_calendar_export_idor.py only
asserts insert was called but doesn't verify the write-back used user-scoped
filters; add an assertion that service.events.return_value.update was called
once with filters including both "id" and "user_id" (or that the call
args/kwargs contain filters={"id": <expected>, "user_id": <expected>}),
alongside the existing insert assertion so the test enforces the update(...,
filters={id,user_id}) contract and prevents regression of owner-scoping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ada72b7-83e3-4348-9906-49f997d6cfed

📥 Commits

Reviewing files that changed from the base of the PR and between a27b688 and 488a201.

📒 Files selected for processing (2)
  • backend/routes/calendar.py
  • backend/tests/test_calendar_export_idor.py

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 97b3aaf into mainJun 14, 2026
6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the security/123-calendar-export-idor branch June 14, 2026 02:17
Sign up for freeto 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.

[P0] calendar.export_to_google cross-user IDOR leaks decrypted private notes

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(security): scope calendar export by user_id — close cross-user IDOR (#123) - #224

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor
Jun 14, 2026
Merged

fix(security): scope calendar export by user_id — close cross-user IDOR (#123)#224
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 13, 2026

Copy link
Copy Markdown
Member

Closes#123. P0 / CRITICAL.

Vulnerability

export_to_google (routes/calendar.py) selected each requested assignment by idwith no user_id scope — read+decrypt another user's private notes, push to the attacker's calendar, stamp google_event_id on the victim's row.

Fix

Per-site user_id scoping on the export select and write-back.

Note on sibling endpoints

update_assignment/delete_assignment/sync_to_google are not vulnerable — each does a user_id-scoped SELECT and returns 404 before its (currently id-only) write, so a non-owned id never reaches the mutation. Their write filters are scoped-by-read-guard, not scoped-by-filter; tightening those write filters as defense-in-depth is a tracked follow-up, not part of this PR.

Negative test (cross-user, fails pre-fix)

tests/test_calendar_export_idor.py: attacker authenticated as self targets victim's assignment id → exported_count==0, no decrypt/insert/update; control test confirms owner can still export. Fails pre-fix (exported_count==1).

⚠️ Auth-boundary change.

…DOR (#123)
export_to_google selected each assignment by id alone, so an authenticated
caller could pass another user's assignment UUIDs to read and decrypt their
private notes, push them into the caller's Google Calendar, and stamp
google_event_id onto the victim's row (data corruption). require_self only
validated the body's user_id, not the ids.
Scope both the select and the write-back update by user_id (matching the sync/
update/delete siblings): a non-owned id now returns no row and is skipped.
Test exercises the exact cross-user path — attacker authenticated as self,
targeting the victim's assignment id — and asserts nothing is decrypted,
exported, or stamped. Fails on pre-fix code (exported_count==1).
@coderabbitai

coderabbitaiBot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a critical IDOR vulnerability in the calendar export endpoint by adding user_id scoping to both the assignment row lookup and the google_event_id write-back, complemented by regression tests simulating the attack scenario where an authenticated user attempts to export another user's assignment.

Changes

Calendar Export Cross-User IDOR Fix

Layer / File(s)Summary
Assignment selection and write-back user-id scoping
backend/routes/calendar.py
The /export endpoint adds user_id equality filters to both the assignment row selection (preventing access to another user's assignments) and the subsequent google_event_id update (preventing data stamping on rows not owned by the caller), with inline comments documenting the defense-in-depth approach.
IDOR regression test suite
backend/tests/test_calendar_export_idor.py
New test module provides regression coverage with a row-scoped mock select helper that simulates real database row-level access rules, an IDOR attack test that verifies cross-user export requests return zero exports with no side effects, and a control test confirming owner-scoped exports function correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A calendar breach, oh what a sight,
User IDs scoped—now the exports are tight!
Mock helpers mock, the tests stand tall,
No more notes leaked across the hall! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 40.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly summarizes the main security fix: adding user_id scoping to close a cross-user IDOR vulnerability in calendar export.
Linked Issues check✅ PassedThe code changes fully satisfy issue #123: user_id scoping added to both select filter and update query in calendar.export, matching sibling endpoints and preventing cross-user access.
Out of Scope Changes check✅ PassedAll changes are directly in-scope: the fix targets the exact vulnerability in export_to_google, and the new test module provides regression coverage for the cross-user IDOR issue.
Description check✅ PassedThe PR description comprehensively covers the vulnerability, fix, testing approach, and auth-boundary implications with clear references to the code changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/123-calendar-export-idor

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 and usage tips.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend488a201Commit Preview URL

Branch Preview URL
Jun 13 2026, 04:56 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/test_calendar_export_idor.py (1)

96-99: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert write-back remains user-scoped in the owner-path test.

This test validates export success but not the update(..., filters={id,user_id}) contract, so the write-back scoping could regress unnoticed.

Suggested test hardening
 assert r.status_code == 200
assert r.json()["exported_count"] == 1
service.events.return_value.insert.assert_called_once()
+ t.return_value.update.assert_called_once()+ _, update_kwargs = t.return_value.update.call_args+ assert update_kwargs["filters"] == {+ "id": "eq.my_assignment",+ "user_id": f"eq.{VICTIM}",+ }
🤖 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 `@backend/tests/test_calendar_export_idor.py` around lines 96 - 99, The test at
backend/tests/test_calendar_export_idor.py only asserts insert was called but
doesn't verify the write-back used user-scoped filters; add an assertion that
service.events.return_value.update was called once with filters including both
"id" and "user_id" (or that the call args/kwargs contain filters={"id":
<expected>, "user_id": <expected>}), alongside the existing insert assertion so
the test enforces the update(..., filters={id,user_id}) contract and prevents
regression of owner-scoping.
🤖 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.
Outside diff comments:
In `@backend/tests/test_calendar_export_idor.py`:
- Around line 96-99: The test at backend/tests/test_calendar_export_idor.py only
asserts insert was called but doesn't verify the write-back used user-scoped
filters; add an assertion that service.events.return_value.update was called
once with filters including both "id" and "user_id" (or that the call
args/kwargs contain filters={"id": <expected>, "user_id": <expected>}),
alongside the existing insert assertion so the test enforces the update(...,
filters={id,user_id}) contract and prevents regression of owner-scoping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ada72b7-83e3-4348-9906-49f997d6cfed

📥 Commits

Reviewing files that changed from the base of the PR and between a27b688 and 488a201.

📒 Files selected for processing (2)
  • backend/routes/calendar.py
  • backend/tests/test_calendar_export_idor.py

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 97b3aaf into mainJun 14, 2026
6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the security/123-calendar-export-idor branch June 14, 2026 02:17
Sign up for freeto 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.

[P0] calendar.export_to_google cross-user IDOR leaks decrypted private notes

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(security): scope calendar export by user_id — close cross-user IDOR (#123) - #224

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor
Jun 14, 2026
Merged

fix(security): scope calendar export by user_id — close cross-user IDOR (#123)#224
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
security/123-calendar-export-idor

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 13, 2026

Copy link
Copy Markdown
Member

Closes#123. P0 / CRITICAL.

Vulnerability

export_to_google (routes/calendar.py) selected each requested assignment by idwith no user_id scope — read+decrypt another user's private notes, push to the attacker's calendar, stamp google_event_id on the victim's row.

Fix

Per-site user_id scoping on the export select and write-back.

Note on sibling endpoints

update_assignment/delete_assignment/sync_to_google are not vulnerable — each does a user_id-scoped SELECT and returns 404 before its (currently id-only) write, so a non-owned id never reaches the mutation. Their write filters are scoped-by-read-guard, not scoped-by-filter; tightening those write filters as defense-in-depth is a tracked follow-up, not part of this PR.

Negative test (cross-user, fails pre-fix)

tests/test_calendar_export_idor.py: attacker authenticated as self targets victim's assignment id → exported_count==0, no decrypt/insert/update; control test confirms owner can still export. Fails pre-fix (exported_count==1).

⚠️ Auth-boundary change.

…DOR (#123)
export_to_google selected each assignment by id alone, so an authenticated
caller could pass another user's assignment UUIDs to read and decrypt their
private notes, push them into the caller's Google Calendar, and stamp
google_event_id onto the victim's row (data corruption). require_self only
validated the body's user_id, not the ids.
Scope both the select and the write-back update by user_id (matching the sync/
update/delete siblings): a non-owned id now returns no row and is skipped.
Test exercises the exact cross-user path — attacker authenticated as self,
targeting the victim's assignment id — and asserts nothing is decrypted,
exported, or stamped. Fails on pre-fix code (exported_count==1).
@coderabbitai

coderabbitaiBot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR fixes a critical IDOR vulnerability in the calendar export endpoint by adding user_id scoping to both the assignment row lookup and the google_event_id write-back, complemented by regression tests simulating the attack scenario where an authenticated user attempts to export another user's assignment.

Changes

Calendar Export Cross-User IDOR Fix

Layer / File(s)Summary
Assignment selection and write-back user-id scoping
backend/routes/calendar.py
The /export endpoint adds user_id equality filters to both the assignment row selection (preventing access to another user's assignments) and the subsequent google_event_id update (preventing data stamping on rows not owned by the caller), with inline comments documenting the defense-in-depth approach.
IDOR regression test suite
backend/tests/test_calendar_export_idor.py
New test module provides regression coverage with a row-scoped mock select helper that simulates real database row-level access rules, an IDOR attack test that verifies cross-user export requests return zero exports with no side effects, and a control test confirming owner-scoped exports function correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A calendar breach, oh what a sight,
User IDs scoped—now the exports are tight!
Mock helpers mock, the tests stand tall,
No more notes leaked across the hall! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 40.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly summarizes the main security fix: adding user_id scoping to close a cross-user IDOR vulnerability in calendar export.
Linked Issues check✅ PassedThe code changes fully satisfy issue #123: user_id scoping added to both select filter and update query in calendar.export, matching sibling endpoints and preventing cross-user access.
Out of Scope Changes check✅ PassedAll changes are directly in-scope: the fix targets the exact vulnerability in export_to_google, and the new test module provides regression coverage for the cross-user IDOR issue.
Description check✅ PassedThe PR description comprehensively covers the vulnerability, fix, testing approach, and auth-boundary implications with clear references to the code changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/123-calendar-export-idor

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 and usage tips.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend488a201Commit Preview URL

Branch Preview URL
Jun 13 2026, 04:56 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/test_calendar_export_idor.py (1)

96-99: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert write-back remains user-scoped in the owner-path test.

This test validates export success but not the update(..., filters={id,user_id}) contract, so the write-back scoping could regress unnoticed.

Suggested test hardening
 assert r.status_code == 200
assert r.json()["exported_count"] == 1
service.events.return_value.insert.assert_called_once()
+ t.return_value.update.assert_called_once()+ _, update_kwargs = t.return_value.update.call_args+ assert update_kwargs["filters"] == {+ "id": "eq.my_assignment",+ "user_id": f"eq.{VICTIM}",+ }
🤖 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 `@backend/tests/test_calendar_export_idor.py` around lines 96 - 99, The test at
backend/tests/test_calendar_export_idor.py only asserts insert was called but
doesn't verify the write-back used user-scoped filters; add an assertion that
service.events.return_value.update was called once with filters including both
"id" and "user_id" (or that the call args/kwargs contain filters={"id":
<expected>, "user_id": <expected>}), alongside the existing insert assertion so
the test enforces the update(..., filters={id,user_id}) contract and prevents
regression of owner-scoping.
🤖 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.
Outside diff comments:
In `@backend/tests/test_calendar_export_idor.py`:
- Around line 96-99: The test at backend/tests/test_calendar_export_idor.py only
asserts insert was called but doesn't verify the write-back used user-scoped
filters; add an assertion that service.events.return_value.update was called
once with filters including both "id" and "user_id" (or that the call
args/kwargs contain filters={"id": <expected>, "user_id": <expected>}),
alongside the existing insert assertion so the test enforces the update(...,
filters={id,user_id}) contract and prevents regression of owner-scoping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ada72b7-83e3-4348-9906-49f997d6cfed

📥 Commits

Reviewing files that changed from the base of the PR and between a27b688 and 488a201.

📒 Files selected for processing (2)
  • backend/routes/calendar.py
  • backend/tests/test_calendar_export_idor.py

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit 97b3aaf into mainJun 14, 2026
6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the security/123-calendar-export-idor branch June 14, 2026 02:17
Sign up for freeto 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.

[P0] calendar.export_to_google cross-user IDOR leaks decrypted private notes

1 participant

@Jose-Gael-Cruz-Lopez