fix(security): final storage lockdown for #231 (private issues-media-files + drop anon policies) - #304

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown
Jul 2, 2026
Merged

fix(security): final storage lockdown for #231 (private issues-media-files + drop anon policies)#304
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 2, 2026

Copy link
Copy Markdown
Member

Closes the last anon storage surface from #231. Investigation found the rest already shipped — this is the small remaining lockdown, delivered as migration 0029_storage_lockdown_231.sql (not yet applied to prod).

Verified live state (prod, read-only, 2026-07-01)

BucketpublicNote
application_resumes (résumés, 13 objs)falsealready private — résumé-PII exposure is closed
avatarstrueintended public read
issues-media-files (screenshots, 2 objs ~7 MB)truethe only residual: public=true + 2 anon storage.objects policies

The triage note ("application_resumes still public-read") and the old plan doc were stale — I've updated storage-hardening-plan.md to match reality.

What already shipped (no change here)

  • feedback.pyPOST /api/issue-reports/screenshot — auth-gated, service-role upload, content-type + 5 MB validated; ReportIssueFlow.tsx posts to it (no anon storage client).
  • application_resumes private + backend/service-key upload (careers.py).
  • Anon "Allow uploads" already scoped to issues-media-files (was global).

This migration (0029)

  1. issues-media-filesprivate (application_resumes already private → no-op).
  2. Adds file_size_limit = 5 MB + mime allowlist on issues-media-files (defence in depth; the endpoint already validates).
  3. Drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow public read" SELECT).

Why it's safe

  • Uploads use the service role (bypasses storage RLS) → dropping anon INSERT can't break them.
  • Screenshots are reviewed via the dashboard / signed URLs — no code reads issues-media-files via getPublicUrl (verified) → private-flip can't break review.
  • avatars untouched. Idempotent across envs (WHERE no-ops on absent buckets; DROP … IF EXISTS).

Apply notes (not auto-applied)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Strengthened file storage access controls.
    • Résumé uploads are now private.
    • Media uploads are limited to approved image types and a 5MB maximum size.
  • Bug Fixes

    • Removed anonymous public access to storage objects, reducing unintended file exposure.
  • Documentation

    • Updated the storage hardening notes to reflect current rollout status and upload rules.

…-files + drop anon policies
Closes the last anon storage surface from #231. The rest already shipped:
application_resumes is private (backend/service-key via careers.py), issue-report
screenshots upload through the auth-gated POST /api/issue-reports/screenshot
(service role, size+mime validated; feedback.py), and ReportIssueFlow.tsx no
longer uses the anon storage client.
Migration 0029 (verified against prod, read-only, before writing):
- makes issues-media-files private (application_resumes already private → no-op),
- caps it at 5 MB + a mime allowlist matching the upload endpoint,
- drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow
public read" SELECT) the app no longer relies on.
Safe: uploads use the service role (bypasses storage RLS) and review is via the
dashboard / signed URLs, so nothing reads issues-media-files via a public URL.
Idempotent across environments (WHERE no-ops on absent buckets; DROP ... IF EXISTS).
NOT yet applied to prod — apply via `python -m db.migrate` (see the migration's
privilege note re: the storage schema).
Also updates docs/security/storage-hardening-plan.md to reflect the shipped state
(résumé-PII exposure already closed; only this lockdown remained).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a SQL migration that hardens Supabase Storage by making the application_resumes bucket private, restricting issues-media-files uploads to 5MB and specific image MIME types, and dropping anonymous upload/public-read policies. Documentation is updated to reflect this progress.

Changes

Storage Hardening

Layer / File(s)Summary
Storage lockdown SQL migration
backend/db/migrations/0029_storage_lockdown_231.sql
New migration sets application_resumes to private, applies 5MB size limit and MIME allowlist to issues-media-files, and drops anonymous "Allow uploads"/"Allow public read" storage.objects policies idempotently.
Storage hardening plan documentation update
docs/security/storage-hardening-plan.md
Updates plan status from draft to shipped-progress, marks résumé PII exposure closed, notes remaining pending SQL lockdown, and revises the SQL guidance to reference the canonical migration file and its size/MIME behavior.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • SaplingLearn/Sapling#239: The upload endpoint in that PR validates content-type/size against the same issues-media-files bucket restrictions this migration enforces server-side.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main change: final storage lockdown for #231.
Description check✅ PassedThe description covers the summary, changes, issue context, and reviewer notes; only some template sections like testing/screenshots are unfilled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/231-storage-lockdown

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.

@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
frontend-staging16da237Commit Preview URL

Branch Preview URL
Jul 02 2026, 03:09 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.

Actionable comments posted: 1

🤖 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 `@docs/security/storage-hardening-plan.md`:
- Line 5: The heading hierarchy in the storage hardening plan skips from the
top-level title straight to the “Already shipped” section, so update the
markdown heading under the document title from h3 to h2 in the section
identified by “Already shipped” to satisfy markdownlint, or insert an
intervening h2 before it if needed.
🪄 Autofix (Beta)

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: 6ad50e64-ea64-4613-ae78-855401f3268f

📥 Commits

Reviewing files that changed from the base of the PR and between 5de742e and 16da237.

📒 Files selected for processing (2)
  • backend/db/migrations/0029_storage_lockdown_231.sql
  • docs/security/storage-hardening-plan.md

**Status (updated 2026-07-01): most of this plan has SHIPPED. The résumé-PII exposure is closed. Only the final SQL lockdown of `issues-media-files` remains — now in `backend/db/migrations/0029_storage_lockdown_231.sql` (pending review + apply; not yet run on prod).**

## Live findings (Sapling prod, read-only)
### Already shipped

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an h2 here instead of skipping straight to h3.

Markdownlint flags the heading-level jump from the h1 title to ### Already shipped. Change it to ## Already shipped (or add an h2 section above).

Suggested fix
-### Already shipped+## Already shipped
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
###Already shipped
## Already shipped
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 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 `@docs/security/storage-hardening-plan.md` at line 5, The heading hierarchy in
the storage hardening plan skips from the top-level title straight to the
“Already shipped” section, so update the markdown heading under the document
title from h3 to h2 in the section identified by “Already shipped” to satisfy
markdownlint, or insert an intervening h2 before it if needed.

Source: Linters/SAST tools

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit fc46b80 into mainJul 2, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/231-storage-lockdown branch August 2, 2026 18:30
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.

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): final storage lockdown for #231 (private issues-media-files + drop anon policies) - #304

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown
Jul 2, 2026
Merged

fix(security): final storage lockdown for #231 (private issues-media-files + drop anon policies)#304
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 2, 2026

Copy link
Copy Markdown
Member

Closes the last anon storage surface from #231. Investigation found the rest already shipped — this is the small remaining lockdown, delivered as migration 0029_storage_lockdown_231.sql (not yet applied to prod).

Verified live state (prod, read-only, 2026-07-01)

BucketpublicNote
application_resumes (résumés, 13 objs)falsealready private — résumé-PII exposure is closed
avatarstrueintended public read
issues-media-files (screenshots, 2 objs ~7 MB)truethe only residual: public=true + 2 anon storage.objects policies

The triage note ("application_resumes still public-read") and the old plan doc were stale — I've updated storage-hardening-plan.md to match reality.

What already shipped (no change here)

  • feedback.pyPOST /api/issue-reports/screenshot — auth-gated, service-role upload, content-type + 5 MB validated; ReportIssueFlow.tsx posts to it (no anon storage client).
  • application_resumes private + backend/service-key upload (careers.py).
  • Anon "Allow uploads" already scoped to issues-media-files (was global).

This migration (0029)

  1. issues-media-filesprivate (application_resumes already private → no-op).
  2. Adds file_size_limit = 5 MB + mime allowlist on issues-media-files (defence in depth; the endpoint already validates).
  3. Drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow public read" SELECT).

Why it's safe

  • Uploads use the service role (bypasses storage RLS) → dropping anon INSERT can't break them.
  • Screenshots are reviewed via the dashboard / signed URLs — no code reads issues-media-files via getPublicUrl (verified) → private-flip can't break review.
  • avatars untouched. Idempotent across envs (WHERE no-ops on absent buckets; DROP … IF EXISTS).

Apply notes (not auto-applied)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Strengthened file storage access controls.
    • Résumé uploads are now private.
    • Media uploads are limited to approved image types and a 5MB maximum size.
  • Bug Fixes

    • Removed anonymous public access to storage objects, reducing unintended file exposure.
  • Documentation

    • Updated the storage hardening notes to reflect current rollout status and upload rules.

…-files + drop anon policies
Closes the last anon storage surface from #231. The rest already shipped:
application_resumes is private (backend/service-key via careers.py), issue-report
screenshots upload through the auth-gated POST /api/issue-reports/screenshot
(service role, size+mime validated; feedback.py), and ReportIssueFlow.tsx no
longer uses the anon storage client.
Migration 0029 (verified against prod, read-only, before writing):
- makes issues-media-files private (application_resumes already private → no-op),
- caps it at 5 MB + a mime allowlist matching the upload endpoint,
- drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow
public read" SELECT) the app no longer relies on.
Safe: uploads use the service role (bypasses storage RLS) and review is via the
dashboard / signed URLs, so nothing reads issues-media-files via a public URL.
Idempotent across environments (WHERE no-ops on absent buckets; DROP ... IF EXISTS).
NOT yet applied to prod — apply via `python -m db.migrate` (see the migration's
privilege note re: the storage schema).
Also updates docs/security/storage-hardening-plan.md to reflect the shipped state
(résumé-PII exposure already closed; only this lockdown remained).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a SQL migration that hardens Supabase Storage by making the application_resumes bucket private, restricting issues-media-files uploads to 5MB and specific image MIME types, and dropping anonymous upload/public-read policies. Documentation is updated to reflect this progress.

Changes

Storage Hardening

Layer / File(s)Summary
Storage lockdown SQL migration
backend/db/migrations/0029_storage_lockdown_231.sql
New migration sets application_resumes to private, applies 5MB size limit and MIME allowlist to issues-media-files, and drops anonymous "Allow uploads"/"Allow public read" storage.objects policies idempotently.
Storage hardening plan documentation update
docs/security/storage-hardening-plan.md
Updates plan status from draft to shipped-progress, marks résumé PII exposure closed, notes remaining pending SQL lockdown, and revises the SQL guidance to reference the canonical migration file and its size/MIME behavior.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • SaplingLearn/Sapling#239: The upload endpoint in that PR validates content-type/size against the same issues-media-files bucket restrictions this migration enforces server-side.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main change: final storage lockdown for #231.
Description check✅ PassedThe description covers the summary, changes, issue context, and reviewer notes; only some template sections like testing/screenshots are unfilled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/231-storage-lockdown

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.

@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
frontend-staging16da237Commit Preview URL

Branch Preview URL
Jul 02 2026, 03:09 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.

Actionable comments posted: 1

🤖 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 `@docs/security/storage-hardening-plan.md`:
- Line 5: The heading hierarchy in the storage hardening plan skips from the
top-level title straight to the “Already shipped” section, so update the
markdown heading under the document title from h3 to h2 in the section
identified by “Already shipped” to satisfy markdownlint, or insert an
intervening h2 before it if needed.
🪄 Autofix (Beta)

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: 6ad50e64-ea64-4613-ae78-855401f3268f

📥 Commits

Reviewing files that changed from the base of the PR and between 5de742e and 16da237.

📒 Files selected for processing (2)
  • backend/db/migrations/0029_storage_lockdown_231.sql
  • docs/security/storage-hardening-plan.md

**Status (updated 2026-07-01): most of this plan has SHIPPED. The résumé-PII exposure is closed. Only the final SQL lockdown of `issues-media-files` remains — now in `backend/db/migrations/0029_storage_lockdown_231.sql` (pending review + apply; not yet run on prod).**

## Live findings (Sapling prod, read-only)
### Already shipped

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an h2 here instead of skipping straight to h3.

Markdownlint flags the heading-level jump from the h1 title to ### Already shipped. Change it to ## Already shipped (or add an h2 section above).

Suggested fix
-### Already shipped+## Already shipped
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
###Already shipped
## Already shipped
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 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 `@docs/security/storage-hardening-plan.md` at line 5, The heading hierarchy in
the storage hardening plan skips from the top-level title straight to the
“Already shipped” section, so update the markdown heading under the document
title from h3 to h2 in the section identified by “Already shipped” to satisfy
markdownlint, or insert an intervening h2 before it if needed.

Source: Linters/SAST tools

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit fc46b80 into mainJul 2, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/231-storage-lockdown branch August 2, 2026 18:30
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.

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): final storage lockdown for #231 (private issues-media-files + drop anon policies) - #304

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown
Jul 2, 2026
Merged

fix(security): final storage lockdown for #231 (private issues-media-files + drop anon policies)#304
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 2, 2026

Copy link
Copy Markdown
Member

Closes the last anon storage surface from #231. Investigation found the rest already shipped — this is the small remaining lockdown, delivered as migration 0029_storage_lockdown_231.sql (not yet applied to prod).

Verified live state (prod, read-only, 2026-07-01)

BucketpublicNote
application_resumes (résumés, 13 objs)falsealready private — résumé-PII exposure is closed
avatarstrueintended public read
issues-media-files (screenshots, 2 objs ~7 MB)truethe only residual: public=true + 2 anon storage.objects policies

The triage note ("application_resumes still public-read") and the old plan doc were stale — I've updated storage-hardening-plan.md to match reality.

What already shipped (no change here)

  • feedback.pyPOST /api/issue-reports/screenshot — auth-gated, service-role upload, content-type + 5 MB validated; ReportIssueFlow.tsx posts to it (no anon storage client).
  • application_resumes private + backend/service-key upload (careers.py).
  • Anon "Allow uploads" already scoped to issues-media-files (was global).

This migration (0029)

  1. issues-media-filesprivate (application_resumes already private → no-op).
  2. Adds file_size_limit = 5 MB + mime allowlist on issues-media-files (defence in depth; the endpoint already validates).
  3. Drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow public read" SELECT).

Why it's safe

  • Uploads use the service role (bypasses storage RLS) → dropping anon INSERT can't break them.
  • Screenshots are reviewed via the dashboard / signed URLs — no code reads issues-media-files via getPublicUrl (verified) → private-flip can't break review.
  • avatars untouched. Idempotent across envs (WHERE no-ops on absent buckets; DROP … IF EXISTS).

Apply notes (not auto-applied)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Strengthened file storage access controls.
    • Résumé uploads are now private.
    • Media uploads are limited to approved image types and a 5MB maximum size.
  • Bug Fixes

    • Removed anonymous public access to storage objects, reducing unintended file exposure.
  • Documentation

    • Updated the storage hardening notes to reflect current rollout status and upload rules.

…-files + drop anon policies
Closes the last anon storage surface from #231. The rest already shipped:
application_resumes is private (backend/service-key via careers.py), issue-report
screenshots upload through the auth-gated POST /api/issue-reports/screenshot
(service role, size+mime validated; feedback.py), and ReportIssueFlow.tsx no
longer uses the anon storage client.
Migration 0029 (verified against prod, read-only, before writing):
- makes issues-media-files private (application_resumes already private → no-op),
- caps it at 5 MB + a mime allowlist matching the upload endpoint,
- drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow
public read" SELECT) the app no longer relies on.
Safe: uploads use the service role (bypasses storage RLS) and review is via the
dashboard / signed URLs, so nothing reads issues-media-files via a public URL.
Idempotent across environments (WHERE no-ops on absent buckets; DROP ... IF EXISTS).
NOT yet applied to prod — apply via `python -m db.migrate` (see the migration's
privilege note re: the storage schema).
Also updates docs/security/storage-hardening-plan.md to reflect the shipped state
(résumé-PII exposure already closed; only this lockdown remained).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a SQL migration that hardens Supabase Storage by making the application_resumes bucket private, restricting issues-media-files uploads to 5MB and specific image MIME types, and dropping anonymous upload/public-read policies. Documentation is updated to reflect this progress.

Changes

Storage Hardening

Layer / File(s)Summary
Storage lockdown SQL migration
backend/db/migrations/0029_storage_lockdown_231.sql
New migration sets application_resumes to private, applies 5MB size limit and MIME allowlist to issues-media-files, and drops anonymous "Allow uploads"/"Allow public read" storage.objects policies idempotently.
Storage hardening plan documentation update
docs/security/storage-hardening-plan.md
Updates plan status from draft to shipped-progress, marks résumé PII exposure closed, notes remaining pending SQL lockdown, and revises the SQL guidance to reference the canonical migration file and its size/MIME behavior.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • SaplingLearn/Sapling#239: The upload endpoint in that PR validates content-type/size against the same issues-media-files bucket restrictions this migration enforces server-side.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main change: final storage lockdown for #231.
Description check✅ PassedThe description covers the summary, changes, issue context, and reviewer notes; only some template sections like testing/screenshots are unfilled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/231-storage-lockdown

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.

@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
frontend-staging16da237Commit Preview URL

Branch Preview URL
Jul 02 2026, 03:09 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.

Actionable comments posted: 1

🤖 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 `@docs/security/storage-hardening-plan.md`:
- Line 5: The heading hierarchy in the storage hardening plan skips from the
top-level title straight to the “Already shipped” section, so update the
markdown heading under the document title from h3 to h2 in the section
identified by “Already shipped” to satisfy markdownlint, or insert an
intervening h2 before it if needed.
🪄 Autofix (Beta)

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: 6ad50e64-ea64-4613-ae78-855401f3268f

📥 Commits

Reviewing files that changed from the base of the PR and between 5de742e and 16da237.

📒 Files selected for processing (2)
  • backend/db/migrations/0029_storage_lockdown_231.sql
  • docs/security/storage-hardening-plan.md

**Status (updated 2026-07-01): most of this plan has SHIPPED. The résumé-PII exposure is closed. Only the final SQL lockdown of `issues-media-files` remains — now in `backend/db/migrations/0029_storage_lockdown_231.sql` (pending review + apply; not yet run on prod).**

## Live findings (Sapling prod, read-only)
### Already shipped

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an h2 here instead of skipping straight to h3.

Markdownlint flags the heading-level jump from the h1 title to ### Already shipped. Change it to ## Already shipped (or add an h2 section above).

Suggested fix
-### Already shipped+## Already shipped
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
###Already shipped
## Already shipped
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 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 `@docs/security/storage-hardening-plan.md` at line 5, The heading hierarchy in
the storage hardening plan skips from the top-level title straight to the
“Already shipped” section, so update the markdown heading under the document
title from h3 to h2 in the section identified by “Already shipped” to satisfy
markdownlint, or insert an intervening h2 before it if needed.

Source: Linters/SAST tools

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit fc46b80 into mainJul 2, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/231-storage-lockdown branch August 2, 2026 18:30
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.

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): final storage lockdown for #231 (private issues-media-files + drop anon policies) - #304

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown
Jul 2, 2026
Merged

fix(security): final storage lockdown for #231 (private issues-media-files + drop anon policies)#304
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 2, 2026

Copy link
Copy Markdown
Member

Closes the last anon storage surface from #231. Investigation found the rest already shipped — this is the small remaining lockdown, delivered as migration 0029_storage_lockdown_231.sql (not yet applied to prod).

Verified live state (prod, read-only, 2026-07-01)

BucketpublicNote
application_resumes (résumés, 13 objs)falsealready private — résumé-PII exposure is closed
avatarstrueintended public read
issues-media-files (screenshots, 2 objs ~7 MB)truethe only residual: public=true + 2 anon storage.objects policies

The triage note ("application_resumes still public-read") and the old plan doc were stale — I've updated storage-hardening-plan.md to match reality.

What already shipped (no change here)

  • feedback.pyPOST /api/issue-reports/screenshot — auth-gated, service-role upload, content-type + 5 MB validated; ReportIssueFlow.tsx posts to it (no anon storage client).
  • application_resumes private + backend/service-key upload (careers.py).
  • Anon "Allow uploads" already scoped to issues-media-files (was global).

This migration (0029)

  1. issues-media-filesprivate (application_resumes already private → no-op).
  2. Adds file_size_limit = 5 MB + mime allowlist on issues-media-files (defence in depth; the endpoint already validates).
  3. Drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow public read" SELECT).

Why it's safe

  • Uploads use the service role (bypasses storage RLS) → dropping anon INSERT can't break them.
  • Screenshots are reviewed via the dashboard / signed URLs — no code reads issues-media-files via getPublicUrl (verified) → private-flip can't break review.
  • avatars untouched. Idempotent across envs (WHERE no-ops on absent buckets; DROP … IF EXISTS).

Apply notes (not auto-applied)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Strengthened file storage access controls.
    • Résumé uploads are now private.
    • Media uploads are limited to approved image types and a 5MB maximum size.
  • Bug Fixes

    • Removed anonymous public access to storage objects, reducing unintended file exposure.
  • Documentation

    • Updated the storage hardening notes to reflect current rollout status and upload rules.

…-files + drop anon policies
Closes the last anon storage surface from #231. The rest already shipped:
application_resumes is private (backend/service-key via careers.py), issue-report
screenshots upload through the auth-gated POST /api/issue-reports/screenshot
(service role, size+mime validated; feedback.py), and ReportIssueFlow.tsx no
longer uses the anon storage client.
Migration 0029 (verified against prod, read-only, before writing):
- makes issues-media-files private (application_resumes already private → no-op),
- caps it at 5 MB + a mime allowlist matching the upload endpoint,
- drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow
public read" SELECT) the app no longer relies on.
Safe: uploads use the service role (bypasses storage RLS) and review is via the
dashboard / signed URLs, so nothing reads issues-media-files via a public URL.
Idempotent across environments (WHERE no-ops on absent buckets; DROP ... IF EXISTS).
NOT yet applied to prod — apply via `python -m db.migrate` (see the migration's
privilege note re: the storage schema).
Also updates docs/security/storage-hardening-plan.md to reflect the shipped state
(résumé-PII exposure already closed; only this lockdown remained).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a SQL migration that hardens Supabase Storage by making the application_resumes bucket private, restricting issues-media-files uploads to 5MB and specific image MIME types, and dropping anonymous upload/public-read policies. Documentation is updated to reflect this progress.

Changes

Storage Hardening

Layer / File(s)Summary
Storage lockdown SQL migration
backend/db/migrations/0029_storage_lockdown_231.sql
New migration sets application_resumes to private, applies 5MB size limit and MIME allowlist to issues-media-files, and drops anonymous "Allow uploads"/"Allow public read" storage.objects policies idempotently.
Storage hardening plan documentation update
docs/security/storage-hardening-plan.md
Updates plan status from draft to shipped-progress, marks résumé PII exposure closed, notes remaining pending SQL lockdown, and revises the SQL guidance to reference the canonical migration file and its size/MIME behavior.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • SaplingLearn/Sapling#239: The upload endpoint in that PR validates content-type/size against the same issues-media-files bucket restrictions this migration enforces server-side.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main change: final storage lockdown for #231.
Description check✅ PassedThe description covers the summary, changes, issue context, and reviewer notes; only some template sections like testing/screenshots are unfilled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/231-storage-lockdown

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.

@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
frontend-staging16da237Commit Preview URL

Branch Preview URL
Jul 02 2026, 03:09 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.

Actionable comments posted: 1

🤖 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 `@docs/security/storage-hardening-plan.md`:
- Line 5: The heading hierarchy in the storage hardening plan skips from the
top-level title straight to the “Already shipped” section, so update the
markdown heading under the document title from h3 to h2 in the section
identified by “Already shipped” to satisfy markdownlint, or insert an
intervening h2 before it if needed.
🪄 Autofix (Beta)

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: 6ad50e64-ea64-4613-ae78-855401f3268f

📥 Commits

Reviewing files that changed from the base of the PR and between 5de742e and 16da237.

📒 Files selected for processing (2)
  • backend/db/migrations/0029_storage_lockdown_231.sql
  • docs/security/storage-hardening-plan.md

**Status (updated 2026-07-01): most of this plan has SHIPPED. The résumé-PII exposure is closed. Only the final SQL lockdown of `issues-media-files` remains — now in `backend/db/migrations/0029_storage_lockdown_231.sql` (pending review + apply; not yet run on prod).**

## Live findings (Sapling prod, read-only)
### Already shipped

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an h2 here instead of skipping straight to h3.

Markdownlint flags the heading-level jump from the h1 title to ### Already shipped. Change it to ## Already shipped (or add an h2 section above).

Suggested fix
-### Already shipped+## Already shipped
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
###Already shipped
## Already shipped
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 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 `@docs/security/storage-hardening-plan.md` at line 5, The heading hierarchy in
the storage hardening plan skips from the top-level title straight to the
“Already shipped” section, so update the markdown heading under the document
title from h3 to h2 in the section identified by “Already shipped” to satisfy
markdownlint, or insert an intervening h2 before it if needed.

Source: Linters/SAST tools

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit fc46b80 into mainJul 2, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/231-storage-lockdown branch August 2, 2026 18:30
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.

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): final storage lockdown for #231 (private issues-media-files + drop anon policies) - #304

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown
Jul 2, 2026
Merged

fix(security): final storage lockdown for #231 (private issues-media-files + drop anon policies)#304
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 2, 2026

Copy link
Copy Markdown
Member

Closes the last anon storage surface from #231. Investigation found the rest already shipped — this is the small remaining lockdown, delivered as migration 0029_storage_lockdown_231.sql (not yet applied to prod).

Verified live state (prod, read-only, 2026-07-01)

BucketpublicNote
application_resumes (résumés, 13 objs)falsealready private — résumé-PII exposure is closed
avatarstrueintended public read
issues-media-files (screenshots, 2 objs ~7 MB)truethe only residual: public=true + 2 anon storage.objects policies

The triage note ("application_resumes still public-read") and the old plan doc were stale — I've updated storage-hardening-plan.md to match reality.

What already shipped (no change here)

  • feedback.pyPOST /api/issue-reports/screenshot — auth-gated, service-role upload, content-type + 5 MB validated; ReportIssueFlow.tsx posts to it (no anon storage client).
  • application_resumes private + backend/service-key upload (careers.py).
  • Anon "Allow uploads" already scoped to issues-media-files (was global).

This migration (0029)

  1. issues-media-filesprivate (application_resumes already private → no-op).
  2. Adds file_size_limit = 5 MB + mime allowlist on issues-media-files (defence in depth; the endpoint already validates).
  3. Drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow public read" SELECT).

Why it's safe

  • Uploads use the service role (bypasses storage RLS) → dropping anon INSERT can't break them.
  • Screenshots are reviewed via the dashboard / signed URLs — no code reads issues-media-files via getPublicUrl (verified) → private-flip can't break review.
  • avatars untouched. Idempotent across envs (WHERE no-ops on absent buckets; DROP … IF EXISTS).

Apply notes (not auto-applied)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Strengthened file storage access controls.
    • Résumé uploads are now private.
    • Media uploads are limited to approved image types and a 5MB maximum size.
  • Bug Fixes

    • Removed anonymous public access to storage objects, reducing unintended file exposure.
  • Documentation

    • Updated the storage hardening notes to reflect current rollout status and upload rules.

…-files + drop anon policies
Closes the last anon storage surface from #231. The rest already shipped:
application_resumes is private (backend/service-key via careers.py), issue-report
screenshots upload through the auth-gated POST /api/issue-reports/screenshot
(service role, size+mime validated; feedback.py), and ReportIssueFlow.tsx no
longer uses the anon storage client.
Migration 0029 (verified against prod, read-only, before writing):
- makes issues-media-files private (application_resumes already private → no-op),
- caps it at 5 MB + a mime allowlist matching the upload endpoint,
- drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow
public read" SELECT) the app no longer relies on.
Safe: uploads use the service role (bypasses storage RLS) and review is via the
dashboard / signed URLs, so nothing reads issues-media-files via a public URL.
Idempotent across environments (WHERE no-ops on absent buckets; DROP ... IF EXISTS).
NOT yet applied to prod — apply via `python -m db.migrate` (see the migration's
privilege note re: the storage schema).
Also updates docs/security/storage-hardening-plan.md to reflect the shipped state
(résumé-PII exposure already closed; only this lockdown remained).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a SQL migration that hardens Supabase Storage by making the application_resumes bucket private, restricting issues-media-files uploads to 5MB and specific image MIME types, and dropping anonymous upload/public-read policies. Documentation is updated to reflect this progress.

Changes

Storage Hardening

Layer / File(s)Summary
Storage lockdown SQL migration
backend/db/migrations/0029_storage_lockdown_231.sql
New migration sets application_resumes to private, applies 5MB size limit and MIME allowlist to issues-media-files, and drops anonymous "Allow uploads"/"Allow public read" storage.objects policies idempotently.
Storage hardening plan documentation update
docs/security/storage-hardening-plan.md
Updates plan status from draft to shipped-progress, marks résumé PII exposure closed, notes remaining pending SQL lockdown, and revises the SQL guidance to reference the canonical migration file and its size/MIME behavior.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • SaplingLearn/Sapling#239: The upload endpoint in that PR validates content-type/size against the same issues-media-files bucket restrictions this migration enforces server-side.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main change: final storage lockdown for #231.
Description check✅ PassedThe description covers the summary, changes, issue context, and reviewer notes; only some template sections like testing/screenshots are unfilled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/231-storage-lockdown

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.

@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
frontend-staging16da237Commit Preview URL

Branch Preview URL
Jul 02 2026, 03:09 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.

Actionable comments posted: 1

🤖 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 `@docs/security/storage-hardening-plan.md`:
- Line 5: The heading hierarchy in the storage hardening plan skips from the
top-level title straight to the “Already shipped” section, so update the
markdown heading under the document title from h3 to h2 in the section
identified by “Already shipped” to satisfy markdownlint, or insert an
intervening h2 before it if needed.
🪄 Autofix (Beta)

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: 6ad50e64-ea64-4613-ae78-855401f3268f

📥 Commits

Reviewing files that changed from the base of the PR and between 5de742e and 16da237.

📒 Files selected for processing (2)
  • backend/db/migrations/0029_storage_lockdown_231.sql
  • docs/security/storage-hardening-plan.md

**Status (updated 2026-07-01): most of this plan has SHIPPED. The résumé-PII exposure is closed. Only the final SQL lockdown of `issues-media-files` remains — now in `backend/db/migrations/0029_storage_lockdown_231.sql` (pending review + apply; not yet run on prod).**

## Live findings (Sapling prod, read-only)
### Already shipped

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an h2 here instead of skipping straight to h3.

Markdownlint flags the heading-level jump from the h1 title to ### Already shipped. Change it to ## Already shipped (or add an h2 section above).

Suggested fix
-### Already shipped+## Already shipped
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
###Already shipped
## Already shipped
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 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 `@docs/security/storage-hardening-plan.md` at line 5, The heading hierarchy in
the storage hardening plan skips from the top-level title straight to the
“Already shipped” section, so update the markdown heading under the document
title from h3 to h2 in the section identified by “Already shipped” to satisfy
markdownlint, or insert an intervening h2 before it if needed.

Source: Linters/SAST tools

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit fc46b80 into mainJul 2, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/231-storage-lockdown branch August 2, 2026 18:30
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.

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): final storage lockdown for #231 (private issues-media-files + drop anon policies) - #304

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown
Jul 2, 2026
Merged

fix(security): final storage lockdown for #231 (private issues-media-files + drop anon policies)#304
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 2, 2026

Copy link
Copy Markdown
Member

Closes the last anon storage surface from #231. Investigation found the rest already shipped — this is the small remaining lockdown, delivered as migration 0029_storage_lockdown_231.sql (not yet applied to prod).

Verified live state (prod, read-only, 2026-07-01)

BucketpublicNote
application_resumes (résumés, 13 objs)falsealready private — résumé-PII exposure is closed
avatarstrueintended public read
issues-media-files (screenshots, 2 objs ~7 MB)truethe only residual: public=true + 2 anon storage.objects policies

The triage note ("application_resumes still public-read") and the old plan doc were stale — I've updated storage-hardening-plan.md to match reality.

What already shipped (no change here)

  • feedback.pyPOST /api/issue-reports/screenshot — auth-gated, service-role upload, content-type + 5 MB validated; ReportIssueFlow.tsx posts to it (no anon storage client).
  • application_resumes private + backend/service-key upload (careers.py).
  • Anon "Allow uploads" already scoped to issues-media-files (was global).

This migration (0029)

  1. issues-media-filesprivate (application_resumes already private → no-op).
  2. Adds file_size_limit = 5 MB + mime allowlist on issues-media-files (defence in depth; the endpoint already validates).
  3. Drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow public read" SELECT).

Why it's safe

  • Uploads use the service role (bypasses storage RLS) → dropping anon INSERT can't break them.
  • Screenshots are reviewed via the dashboard / signed URLs — no code reads issues-media-files via getPublicUrl (verified) → private-flip can't break review.
  • avatars untouched. Idempotent across envs (WHERE no-ops on absent buckets; DROP … IF EXISTS).

Apply notes (not auto-applied)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Strengthened file storage access controls.
    • Résumé uploads are now private.
    • Media uploads are limited to approved image types and a 5MB maximum size.
  • Bug Fixes

    • Removed anonymous public access to storage objects, reducing unintended file exposure.
  • Documentation

    • Updated the storage hardening notes to reflect current rollout status and upload rules.

…-files + drop anon policies
Closes the last anon storage surface from #231. The rest already shipped:
application_resumes is private (backend/service-key via careers.py), issue-report
screenshots upload through the auth-gated POST /api/issue-reports/screenshot
(service role, size+mime validated; feedback.py), and ReportIssueFlow.tsx no
longer uses the anon storage client.
Migration 0029 (verified against prod, read-only, before writing):
- makes issues-media-files private (application_resumes already private → no-op),
- caps it at 5 MB + a mime allowlist matching the upload endpoint,
- drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow
public read" SELECT) the app no longer relies on.
Safe: uploads use the service role (bypasses storage RLS) and review is via the
dashboard / signed URLs, so nothing reads issues-media-files via a public URL.
Idempotent across environments (WHERE no-ops on absent buckets; DROP ... IF EXISTS).
NOT yet applied to prod — apply via `python -m db.migrate` (see the migration's
privilege note re: the storage schema).
Also updates docs/security/storage-hardening-plan.md to reflect the shipped state
(résumé-PII exposure already closed; only this lockdown remained).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a SQL migration that hardens Supabase Storage by making the application_resumes bucket private, restricting issues-media-files uploads to 5MB and specific image MIME types, and dropping anonymous upload/public-read policies. Documentation is updated to reflect this progress.

Changes

Storage Hardening

Layer / File(s)Summary
Storage lockdown SQL migration
backend/db/migrations/0029_storage_lockdown_231.sql
New migration sets application_resumes to private, applies 5MB size limit and MIME allowlist to issues-media-files, and drops anonymous "Allow uploads"/"Allow public read" storage.objects policies idempotently.
Storage hardening plan documentation update
docs/security/storage-hardening-plan.md
Updates plan status from draft to shipped-progress, marks résumé PII exposure closed, notes remaining pending SQL lockdown, and revises the SQL guidance to reference the canonical migration file and its size/MIME behavior.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • SaplingLearn/Sapling#239: The upload endpoint in that PR validates content-type/size against the same issues-media-files bucket restrictions this migration enforces server-side.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main change: final storage lockdown for #231.
Description check✅ PassedThe description covers the summary, changes, issue context, and reviewer notes; only some template sections like testing/screenshots are unfilled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/231-storage-lockdown

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.

@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
frontend-staging16da237Commit Preview URL

Branch Preview URL
Jul 02 2026, 03:09 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.

Actionable comments posted: 1

🤖 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 `@docs/security/storage-hardening-plan.md`:
- Line 5: The heading hierarchy in the storage hardening plan skips from the
top-level title straight to the “Already shipped” section, so update the
markdown heading under the document title from h3 to h2 in the section
identified by “Already shipped” to satisfy markdownlint, or insert an
intervening h2 before it if needed.
🪄 Autofix (Beta)

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: 6ad50e64-ea64-4613-ae78-855401f3268f

📥 Commits

Reviewing files that changed from the base of the PR and between 5de742e and 16da237.

📒 Files selected for processing (2)
  • backend/db/migrations/0029_storage_lockdown_231.sql
  • docs/security/storage-hardening-plan.md

**Status (updated 2026-07-01): most of this plan has SHIPPED. The résumé-PII exposure is closed. Only the final SQL lockdown of `issues-media-files` remains — now in `backend/db/migrations/0029_storage_lockdown_231.sql` (pending review + apply; not yet run on prod).**

## Live findings (Sapling prod, read-only)
### Already shipped

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an h2 here instead of skipping straight to h3.

Markdownlint flags the heading-level jump from the h1 title to ### Already shipped. Change it to ## Already shipped (or add an h2 section above).

Suggested fix
-### Already shipped+## Already shipped
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
###Already shipped
## Already shipped
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 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 `@docs/security/storage-hardening-plan.md` at line 5, The heading hierarchy in
the storage hardening plan skips from the top-level title straight to the
“Already shipped” section, so update the markdown heading under the document
title from h3 to h2 in the section identified by “Already shipped” to satisfy
markdownlint, or insert an intervening h2 before it if needed.

Source: Linters/SAST tools

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit fc46b80 into mainJul 2, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/231-storage-lockdown branch August 2, 2026 18:30
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.

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): final storage lockdown for #231 (private issues-media-files + drop anon policies) - #304

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown
Jul 2, 2026
Merged

fix(security): final storage lockdown for #231 (private issues-media-files + drop anon policies)#304
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 2, 2026

Copy link
Copy Markdown
Member

Closes the last anon storage surface from #231. Investigation found the rest already shipped — this is the small remaining lockdown, delivered as migration 0029_storage_lockdown_231.sql (not yet applied to prod).

Verified live state (prod, read-only, 2026-07-01)

BucketpublicNote
application_resumes (résumés, 13 objs)falsealready private — résumé-PII exposure is closed
avatarstrueintended public read
issues-media-files (screenshots, 2 objs ~7 MB)truethe only residual: public=true + 2 anon storage.objects policies

The triage note ("application_resumes still public-read") and the old plan doc were stale — I've updated storage-hardening-plan.md to match reality.

What already shipped (no change here)

  • feedback.pyPOST /api/issue-reports/screenshot — auth-gated, service-role upload, content-type + 5 MB validated; ReportIssueFlow.tsx posts to it (no anon storage client).
  • application_resumes private + backend/service-key upload (careers.py).
  • Anon "Allow uploads" already scoped to issues-media-files (was global).

This migration (0029)

  1. issues-media-filesprivate (application_resumes already private → no-op).
  2. Adds file_size_limit = 5 MB + mime allowlist on issues-media-files (defence in depth; the endpoint already validates).
  3. Drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow public read" SELECT).

Why it's safe

  • Uploads use the service role (bypasses storage RLS) → dropping anon INSERT can't break them.
  • Screenshots are reviewed via the dashboard / signed URLs — no code reads issues-media-files via getPublicUrl (verified) → private-flip can't break review.
  • avatars untouched. Idempotent across envs (WHERE no-ops on absent buckets; DROP … IF EXISTS).

Apply notes (not auto-applied)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Strengthened file storage access controls.
    • Résumé uploads are now private.
    • Media uploads are limited to approved image types and a 5MB maximum size.
  • Bug Fixes

    • Removed anonymous public access to storage objects, reducing unintended file exposure.
  • Documentation

    • Updated the storage hardening notes to reflect current rollout status and upload rules.

…-files + drop anon policies
Closes the last anon storage surface from #231. The rest already shipped:
application_resumes is private (backend/service-key via careers.py), issue-report
screenshots upload through the auth-gated POST /api/issue-reports/screenshot
(service role, size+mime validated; feedback.py), and ReportIssueFlow.tsx no
longer uses the anon storage client.
Migration 0029 (verified against prod, read-only, before writing):
- makes issues-media-files private (application_resumes already private → no-op),
- caps it at 5 MB + a mime allowlist matching the upload endpoint,
- drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow
public read" SELECT) the app no longer relies on.
Safe: uploads use the service role (bypasses storage RLS) and review is via the
dashboard / signed URLs, so nothing reads issues-media-files via a public URL.
Idempotent across environments (WHERE no-ops on absent buckets; DROP ... IF EXISTS).
NOT yet applied to prod — apply via `python -m db.migrate` (see the migration's
privilege note re: the storage schema).
Also updates docs/security/storage-hardening-plan.md to reflect the shipped state
(résumé-PII exposure already closed; only this lockdown remained).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a SQL migration that hardens Supabase Storage by making the application_resumes bucket private, restricting issues-media-files uploads to 5MB and specific image MIME types, and dropping anonymous upload/public-read policies. Documentation is updated to reflect this progress.

Changes

Storage Hardening

Layer / File(s)Summary
Storage lockdown SQL migration
backend/db/migrations/0029_storage_lockdown_231.sql
New migration sets application_resumes to private, applies 5MB size limit and MIME allowlist to issues-media-files, and drops anonymous "Allow uploads"/"Allow public read" storage.objects policies idempotently.
Storage hardening plan documentation update
docs/security/storage-hardening-plan.md
Updates plan status from draft to shipped-progress, marks résumé PII exposure closed, notes remaining pending SQL lockdown, and revises the SQL guidance to reference the canonical migration file and its size/MIME behavior.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • SaplingLearn/Sapling#239: The upload endpoint in that PR validates content-type/size against the same issues-media-files bucket restrictions this migration enforces server-side.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main change: final storage lockdown for #231.
Description check✅ PassedThe description covers the summary, changes, issue context, and reviewer notes; only some template sections like testing/screenshots are unfilled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/231-storage-lockdown

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.

@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
frontend-staging16da237Commit Preview URL

Branch Preview URL
Jul 02 2026, 03:09 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.

Actionable comments posted: 1

🤖 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 `@docs/security/storage-hardening-plan.md`:
- Line 5: The heading hierarchy in the storage hardening plan skips from the
top-level title straight to the “Already shipped” section, so update the
markdown heading under the document title from h3 to h2 in the section
identified by “Already shipped” to satisfy markdownlint, or insert an
intervening h2 before it if needed.
🪄 Autofix (Beta)

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: 6ad50e64-ea64-4613-ae78-855401f3268f

📥 Commits

Reviewing files that changed from the base of the PR and between 5de742e and 16da237.

📒 Files selected for processing (2)
  • backend/db/migrations/0029_storage_lockdown_231.sql
  • docs/security/storage-hardening-plan.md

**Status (updated 2026-07-01): most of this plan has SHIPPED. The résumé-PII exposure is closed. Only the final SQL lockdown of `issues-media-files` remains — now in `backend/db/migrations/0029_storage_lockdown_231.sql` (pending review + apply; not yet run on prod).**

## Live findings (Sapling prod, read-only)
### Already shipped

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an h2 here instead of skipping straight to h3.

Markdownlint flags the heading-level jump from the h1 title to ### Already shipped. Change it to ## Already shipped (or add an h2 section above).

Suggested fix
-### Already shipped+## Already shipped
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
###Already shipped
## Already shipped
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 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 `@docs/security/storage-hardening-plan.md` at line 5, The heading hierarchy in
the storage hardening plan skips from the top-level title straight to the
“Already shipped” section, so update the markdown heading under the document
title from h3 to h2 in the section identified by “Already shipped” to satisfy
markdownlint, or insert an intervening h2 before it if needed.

Source: Linters/SAST tools

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit fc46b80 into mainJul 2, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/231-storage-lockdown branch August 2, 2026 18:30
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.

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): final storage lockdown for #231 (private issues-media-files + drop anon policies) - #304

Merged
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown
Jul 2, 2026
Merged

fix(security): final storage lockdown for #231 (private issues-media-files + drop anon policies)#304
Jose-Gael-Cruz-Lopez merged 1 commit into
mainfrom
fix/231-storage-lockdown

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 2, 2026

Copy link
Copy Markdown
Member

Closes the last anon storage surface from #231. Investigation found the rest already shipped — this is the small remaining lockdown, delivered as migration 0029_storage_lockdown_231.sql (not yet applied to prod).

Verified live state (prod, read-only, 2026-07-01)

BucketpublicNote
application_resumes (résumés, 13 objs)falsealready private — résumé-PII exposure is closed
avatarstrueintended public read
issues-media-files (screenshots, 2 objs ~7 MB)truethe only residual: public=true + 2 anon storage.objects policies

The triage note ("application_resumes still public-read") and the old plan doc were stale — I've updated storage-hardening-plan.md to match reality.

What already shipped (no change here)

  • feedback.pyPOST /api/issue-reports/screenshot — auth-gated, service-role upload, content-type + 5 MB validated; ReportIssueFlow.tsx posts to it (no anon storage client).
  • application_resumes private + backend/service-key upload (careers.py).
  • Anon "Allow uploads" already scoped to issues-media-files (was global).

This migration (0029)

  1. issues-media-filesprivate (application_resumes already private → no-op).
  2. Adds file_size_limit = 5 MB + mime allowlist on issues-media-files (defence in depth; the endpoint already validates).
  3. Drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow public read" SELECT).

Why it's safe

  • Uploads use the service role (bypasses storage RLS) → dropping anon INSERT can't break them.
  • Screenshots are reviewed via the dashboard / signed URLs — no code reads issues-media-files via getPublicUrl (verified) → private-flip can't break review.
  • avatars untouched. Idempotent across envs (WHERE no-ops on absent buckets; DROP … IF EXISTS).

Apply notes (not auto-applied)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Strengthened file storage access controls.
    • Résumé uploads are now private.
    • Media uploads are limited to approved image types and a 5MB maximum size.
  • Bug Fixes

    • Removed anonymous public access to storage objects, reducing unintended file exposure.
  • Documentation

    • Updated the storage hardening notes to reflect current rollout status and upload rules.

…-files + drop anon policies
Closes the last anon storage surface from #231. The rest already shipped:
application_resumes is private (backend/service-key via careers.py), issue-report
screenshots upload through the auth-gated POST /api/issue-reports/screenshot
(service role, size+mime validated; feedback.py), and ReportIssueFlow.tsx no
longer uses the anon storage client.
Migration 0029 (verified against prod, read-only, before writing):
- makes issues-media-files private (application_resumes already private → no-op),
- caps it at 5 MB + a mime allowlist matching the upload endpoint,
- drops the two anon storage.objects policies ("Allow uploads" INSERT / "Allow
public read" SELECT) the app no longer relies on.
Safe: uploads use the service role (bypasses storage RLS) and review is via the
dashboard / signed URLs, so nothing reads issues-media-files via a public URL.
Idempotent across environments (WHERE no-ops on absent buckets; DROP ... IF EXISTS).
NOT yet applied to prod — apply via `python -m db.migrate` (see the migration's
privilege note re: the storage schema).
Also updates docs/security/storage-hardening-plan.md to reflect the shipped state
(résumé-PII exposure already closed; only this lockdown remained).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a SQL migration that hardens Supabase Storage by making the application_resumes bucket private, restricting issues-media-files uploads to 5MB and specific image MIME types, and dropping anonymous upload/public-read policies. Documentation is updated to reflect this progress.

Changes

Storage Hardening

Layer / File(s)Summary
Storage lockdown SQL migration
backend/db/migrations/0029_storage_lockdown_231.sql
New migration sets application_resumes to private, applies 5MB size limit and MIME allowlist to issues-media-files, and drops anonymous "Allow uploads"/"Allow public read" storage.objects policies idempotently.
Storage hardening plan documentation update
docs/security/storage-hardening-plan.md
Updates plan status from draft to shipped-progress, marks résumé PII exposure closed, notes remaining pending SQL lockdown, and revises the SQL guidance to reference the canonical migration file and its size/MIME behavior.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • SaplingLearn/Sapling#239: The upload endpoint in that PR validates content-type/size against the same issues-media-files bucket restrictions this migration enforces server-side.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main change: final storage lockdown for #231.
Description check✅ PassedThe description covers the summary, changes, issue context, and reviewer notes; only some template sections like testing/screenshots are unfilled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/231-storage-lockdown

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.

@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
frontend-staging16da237Commit Preview URL

Branch Preview URL
Jul 02 2026, 03:09 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.

Actionable comments posted: 1

🤖 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 `@docs/security/storage-hardening-plan.md`:
- Line 5: The heading hierarchy in the storage hardening plan skips from the
top-level title straight to the “Already shipped” section, so update the
markdown heading under the document title from h3 to h2 in the section
identified by “Already shipped” to satisfy markdownlint, or insert an
intervening h2 before it if needed.
🪄 Autofix (Beta)

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: 6ad50e64-ea64-4613-ae78-855401f3268f

📥 Commits

Reviewing files that changed from the base of the PR and between 5de742e and 16da237.

📒 Files selected for processing (2)
  • backend/db/migrations/0029_storage_lockdown_231.sql
  • docs/security/storage-hardening-plan.md

**Status (updated 2026-07-01): most of this plan has SHIPPED. The résumé-PII exposure is closed. Only the final SQL lockdown of `issues-media-files` remains — now in `backend/db/migrations/0029_storage_lockdown_231.sql` (pending review + apply; not yet run on prod).**

## Live findings (Sapling prod, read-only)
### Already shipped

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an h2 here instead of skipping straight to h3.

Markdownlint flags the heading-level jump from the h1 title to ### Already shipped. Change it to ## Already shipped (or add an h2 section above).

Suggested fix
-### Already shipped+## Already shipped
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
###Already shipped
## Already shipped
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 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 `@docs/security/storage-hardening-plan.md` at line 5, The heading hierarchy in
the storage hardening plan skips from the top-level title straight to the
“Already shipped” section, so update the markdown heading under the document
title from h3 to h2 in the section identified by “Already shipped” to satisfy
markdownlint, or insert an intervening h2 before it if needed.

Source: Linters/SAST tools

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit fc46b80 into mainJul 2, 2026
6 checks passed
@AndresL230
AndresL230 deleted the fix/231-storage-lockdown branch August 2, 2026 18:30
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.

1 participant

@Jose-Gael-Cruz-Lopez