Skip to content

Fix immediate security findings#733

Merged
JoaquinBN merged 3 commits into
devfrom
JoaquinBN/review-security-findings
Jun 5, 2026
Merged

Fix immediate security findings#733
JoaquinBN merged 3 commits into
devfrom
JoaquinBN/review-security-findings

Conversation

@JoaquinBN

@JoaquinBN JoaquinBN commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Implements the immediate security fixes across SIWE/POAP nonce purpose validation, Markdown sanitization, role profile mutation restrictions, submission update gating, steward review hardening, duplicate-evidence ordering, and removal of the tracked SQLite artifact. Also updates local SIWE config handling for localhost:5173 and adds focused regression coverage for the touched auth, POAP, role, submission, review, duplicate-evidence, and Markdown flows. Verification: backend touched-surface suite passed with 132 tests OK; focused frontend Markdown sanitizer test passed.

Summary by CodeRabbit

  • Bug Fixes

    • Builder/Steward/Validator write actions restricted to staff; profile endpoints return 404 instead of auto-creating
    • POAP wallet recovery now enforces strict SIWE message format and nonce purpose
  • New Features

    • Nonce purpose support added to authentication and recovery flows (separate login vs POAP recovery)
    • Markdown rendering sanitized client-side to block unsafe HTML; DOMPurify dependency added
    • SIWE domain/URI handling tuned for frontend host/port variations
  • Tests

    • Expanded tests for auth, POAP, contributions, validators, stewards, builders, and markdown sanitizer

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 56e55d63-a654-444c-9bdb-d3f937a1bfaa

📥 Commits

Reviewing files that changed from the base of the PR and between f88fa48 and eb30e03.

📒 Files selected for processing (6)
  • backend/.env.example
  • backend/contributions/management/commands/review_submissions.py
  • backend/contributions/tests/test_review_submissions.py
  • backend/validators/tests/test_api.py
  • frontend/src/lib/markdownLoader.js
  • frontend/src/tests/markdownLoader.test.js

📝 Walkthrough

Walkthrough

Adds SIWE nonce purposes and validation helpers, enforces staff-only profile mutations, centralizes submission gating and duplicate-evidence timestamp logic, auto-detects evidence URL types, and sanitizes rendered Markdown on the frontend.

Changes

Ethereum Authentication with Nonce Purposes

Layer / File(s) Summary
Nonce model and purpose field
backend/ethereum_auth/models.py, backend/ethereum_auth/migrations/0002_nonce_purpose.py
Nonce adds PURPOSE_LOGIN and PURPOSE_POAP_RECOVERY, an indexed purpose CharField with choices/default, str updated, and migration adds the column.
SIWE domain and URI computation utilities
backend/ethereum_auth/siwe_utils.py
Adds normalize_origin(), get_expected_siwe_domain(), and get_expected_siwe_uri() to derive normalized expected SIWE domain/URI from settings.
Nonce API endpoints and login verification
backend/ethereum_auth/views.py
get_nonce accepts/validates purpose, stores expires_at and purpose, returns {nonce, purpose}. login parses SIWE via SiweMessage, validates statement/domain/URI, locks nonce by value+purpose, verifies signature, marks nonce used. Legacy eth_account parsing removed.
POAP recovery with purpose-specific nonce
backend/poaps/views.py
Adds RECOVERY_STATEMENT and expected_recovery_* helpers; verify_wallet enforces strict SIWE-like message line checks, normalized URI/version/chain id validation, and locks nonce by value+POAP recovery purpose.
Frontend nonce usage & tests
frontend/src/lib/auth.js, frontend/src/routes/PoapRecovery.svelte, backend/ethereum_auth/tests.py, backend/poaps/tests/test_poaps.py
getNonce(purpose) sends purpose query param; signInWithEthereum calls getNonce('login'); PoapRecovery requests 'poap_recovery'. Tests validate purpose handling and domain/URI expectations.

Profile Mutation Authorization

Layer / File(s) Summary
Builders: staff-only mutations and my_profile 404
backend/builders/views.py, backend/builders/tests.py
BuilderViewSet adds staff-check helpers and denies non-staff write operations; my_profile PATCH now returns 404 when missing. APITestCase added to assert 404/403 and DB state.
Stewards: staff-only mutations and my_profile 404
backend/stewards/views.py, backend/stewards/tests.py
StewardViewSet denies non-staff writes and my_profile PATCH returns 404 when missing. Tests assert forbidden creation/mutation by regular users.
Validators: staff-only mutations, my_profile 404, field restrictions
backend/validators/views.py, backend/validators/tests.py, backend/validators/tests/test_api.py
ValidatorViewSet denies non-staff writes, my_profile PATCH only allows node_version_asimov/node_version_bradbury and returns 404 when missing. Tests updated/added for authorization and field checks.

Contribution Submission and Steward Review Flow

Layer / File(s) Summary
Submission contribution-type validation helper
backend/contributions/views.py
Adds _validate_submission_contribution_type() for mission/type consistency, direct-submission eligibility, optional capacity checks, category prerequisites, required social accounts, and Discord-role requirements with sync/grace handling.
Submission create/update with validation and mission constraints
backend/contributions/views.py
create() calls validation helper; update() forbids mission changes after submission and revalidates, skipping capacity checks when type unchanged.
Steward review with final_contribution_type
backend/contributions/views.py
review() passes submission and request to serializer, resolves final_contribution_type for accept actions and Contribution creation, and copies Evidence with normalized_url.
Serializer validation for review & evidence
backend/contributions/serializers.py
Evidence validation uses context.get('request') fallback; StewardSubmissionReviewSerializer validates final contribution_type, points bounds when type present, and prevents non-staff reassignment of accepted submissions.
Evidence url_type auto-detection and normalization
backend/contributions/models.py, backend/contributions/views.py
Evidence.save() auto-detects url_type when missing; steward accept flow populates normalized_url for copied Evidence.
Duplicate-evidence logic with timestamps
backend/contributions/management/commands/review_submissions.py
Tier 1 duplicate logic uses submission created_at to ensure pending duplicates only reject when the duplicate is older; _build_url_lookup prefetches created_at and url_type__allow_duplicate, skips allow_duplicate URLs, and falls back to detect_url_type when needed.
Contribution tests and fixtures
backend/contributions/tests/*
Adds duplicate-evidence test, updates URL-normalization examples, uses update_or_create for EvidenceURLType fixtures, and extends steward/validator tests for permission, points validation, reassignment, categories, and capacity gating.

Frontend Markdown Sanitization

Layer / File(s) Summary
Markdown HTML sanitization with dompurify
frontend/package.json, frontend/src/lib/markdownLoader.js, frontend/src/tests/markdownLoader.test.js
Adds dompurify dependency; markdownLoader uses DOMPurify.sanitize(marked.parse(...), SANITIZE_CONFIG) and removes prior marked sanitize option. Tests ensure safe content preserved and unsafe tags/attributes/URLs/iframes stripped.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main purpose of the changeset: implementing immediate security fixes across multiple backend and frontend areas.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch JoaquinBN/review-security-findings

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

@coderabbitai coderabbitai Bot 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: 3

Caution

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

⚠️ Outside diff range comments (2)
backend/.env.example (1)

4-4: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add testserver to ALLOWED_HOSTS in the example env.

ALLOWED_HOSTS is missing testserver, which can break test requests that use Django’s default test host.

Suggested change
-ALLOWED_HOSTS=localhost,127.0.0.1
+ALLOWED_HOSTS=localhost,127.0.0.1,testserver

As per coding guidelines: "backend/**/.env*: Add 'testserver' to ALLOWED_HOSTS in .env configuration for tests to work properly".

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

In `@backend/.env.example` at line 4, Update the ALLOWED_HOSTS entry in the
example environment to include "testserver" so Django tests using the default
test host succeed; specifically modify the ALLOWED_HOSTS variable string
(ALLOWED_HOSTS=localhost,127.0.0.1) to also contain testserver (e.g.,
ALLOWED_HOSTS=localhost,127.0.0.1,testserver).
backend/contributions/management/commands/review_submissions.py (1)

184-216: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor skip_pending before consulting url_to_sub_ids.

handle() sets skip_pending_duplicates for --submission-id, but _check_single_url_duplicate() never uses that flag. Single-submission runs will still reject against pending/newer submissions via url_to_sub_ids, which reintroduces the order-dependent behavior this PR is trying to remove.

Suggested fix
     if normalized in accepted_urls:
         return (
             'Reject: Duplicate Submission',
             f'Tier 1 auto-reject: Evidence URL already exists in an '
             f'accepted contribution: {evidence.url[:100]}',
         )
+
+    if skip_pending:
+        return None
+
     # Check pending/accepted submitted contributions (exclude self)
     others = (url_to_sub_ids.get(normalized) or set()) - {submission.id}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/contributions/management/commands/review_submissions.py` around lines
184 - 216, The _check_single_url_duplicate function currently consults
url_to_sub_ids and rejects against pending submissions even when the command set
skip_pending_duplicates; update _check_single_url_duplicate to respect that flag
by filtering out pending submission IDs from others before returning or loading
created_at data: when skip_pending_duplicates is True remove any IDs whose
status is pending/new (e.g. via an existing status map or a DB lookup on
SubmittedContribution.objects.filter(id__in=others).values_list('id','status'))
so that url_to_sub_ids and missing_created_at_ids only include non-pending
submissions; ensure the function signature or caller passes
skip_pending_duplicates (from handle()) if needed and keep the later
duplicate-created_at comparison unchanged.
🤖 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 `@backend/validators/tests/test_api.py`:
- Around line 42-73: Add a negative test to backend/validators/tests/test_api.py
that verifies PATCH /api/v1/validators/me/ rejects unsupported keys: call
self.client.patch('/api/v1/validators/me/', {'unsupported_field': 'x'}) and
assert response.status_code == status.HTTP_400_BAD_REQUEST and that no Validator
was created for self.user; place this alongside
test_patch_validator_profile_does_not_create_missing_profile so the
unsupported-field guard on the view/serializer (PATCH /validators/me/) is
covered and will fail if future changes allow arbitrary keys.

In `@frontend/src/lib/markdownLoader.js`:
- Around line 4-22: Update the SANITIZE_CONFIG object in markdownLoader.js to
use an explicit whitelist and tighter rules: replace or augment FORBID_TAGS with
an ALLOWED_TAGS array listing only the safe HTML tags your markdown needs (e.g.,
p, a, ul, ol, li, strong, em, img, h1-h6, pre, code), add ALLOW_DATA_ATTR: false
to disable data-* attributes, and add an ALLOWED_URI_REGEXP entry that permits
only safe schemes (e.g., ^(https?|mailto):) for link/src attributes; keep
FORBID_ATTR: ['style'] (and remove any overlapping forbidden tags) so DOMPurify
uses the explicit whitelist and URI regexp for stronger defense-in-depth when
sanitizing via SANITIZE_CONFIG.

In `@frontend/src/tests/markdownLoader.test.js`:
- Around line 13-27: Add extra edge-case unit tests to markdownLoader.test.js
that call parseMarkdown and assert the output strips additional XSS vectors:
include inputs with data:text/html URLs, other event handlers (onload,
onmouseover) on multiple elements, <object> and <embed> tags, <form> and <input>
elements, and SVG payloads like <svg onload="...">; for each case assert the
returned HTML does not contain the forbidden tags/attributes/URLs (e.g.,
'data:text/html', 'onload', 'onmouseover', '<object', '<embed', '<form',
'<input', '<svg') and reference the existing FORBID_TAGS behavior to ensure
parity with sanitizer expectations.

---

Outside diff comments:
In `@backend/.env.example`:
- Line 4: Update the ALLOWED_HOSTS entry in the example environment to include
"testserver" so Django tests using the default test host succeed; specifically
modify the ALLOWED_HOSTS variable string (ALLOWED_HOSTS=localhost,127.0.0.1) to
also contain testserver (e.g., ALLOWED_HOSTS=localhost,127.0.0.1,testserver).

In `@backend/contributions/management/commands/review_submissions.py`:
- Around line 184-216: The _check_single_url_duplicate function currently
consults url_to_sub_ids and rejects against pending submissions even when the
command set skip_pending_duplicates; update _check_single_url_duplicate to
respect that flag by filtering out pending submission IDs from others before
returning or loading created_at data: when skip_pending_duplicates is True
remove any IDs whose status is pending/new (e.g. via an existing status map or a
DB lookup on
SubmittedContribution.objects.filter(id__in=others).values_list('id','status'))
so that url_to_sub_ids and missing_created_at_ids only include non-pending
submissions; ensure the function signature or caller passes
skip_pending_duplicates (from handle()) if needed and keep the later
duplicate-created_at comparison unchanged.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3b5b5cce-bee2-403f-8d72-051054b262a0

📥 Commits

Reviewing files that changed from the base of the PR and between 7e42ebf and a9649e5.

⛔ Files ignored due to path filters (2)
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • backend/.env.example
  • backend/2025-05-27.sqlite3
  • backend/builders/tests.py
  • backend/builders/views.py
  • backend/contributions/management/commands/review_submissions.py
  • backend/contributions/models.py
  • backend/contributions/serializers.py
  • backend/contributions/tests/test_review_submissions.py
  • backend/contributions/tests/test_steward_permissions.py
  • backend/contributions/tests/test_validator_category_gating.py
  • backend/contributions/views.py
  • backend/docker-compose.yml
  • backend/ethereum_auth/migrations/0002_nonce_purpose.py
  • backend/ethereum_auth/models.py
  • backend/ethereum_auth/siwe_utils.py
  • backend/ethereum_auth/tests.py
  • backend/ethereum_auth/views.py
  • backend/poaps/tests/test_poaps.py
  • backend/poaps/views.py
  • backend/stewards/tests.py
  • backend/stewards/views.py
  • backend/validators/tests.py
  • backend/validators/tests/test_api.py
  • backend/validators/views.py
  • frontend/package.json
  • frontend/src/lib/auth.js
  • frontend/src/lib/markdownLoader.js
  • frontend/src/routes/PoapRecovery.svelte
  • frontend/src/tests/markdownLoader.test.js

Comment thread backend/validators/tests/test_api.py
Comment thread frontend/src/lib/markdownLoader.js
Comment thread frontend/src/tests/markdownLoader.test.js

@coderabbitai coderabbitai Bot 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

Caution

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

⚠️ Outside diff range comments (2)
backend/contributions/models.py (1)

749-757: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear stale url_type when the URL is removed.

The new auto-detection never resets url_type in the else branch, so an evidence row can become description-only while still carrying its previous URL classification.

🔧 Proposed fix
         else:
+            self.url_type = None
             self.normalized_url = ''
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/contributions/models.py` around lines 749 - 757, The save method in
the Contribution model leaves a stale url_type when self.url is empty; update
the else branch in save to clear the URL classification by setting self.url_type
= None and self.url_type_id = None (and ensure self.normalized_url = ''
remains), so removal of a URL resets both the foreign-key object and its id;
reference the save method and the fields/methods url, url_type, url_type_id,
normalized_url, detect_url_type, normalize_url when making this change.
backend/contributions/views.py (1)

47-47: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Backfill url_type when copying accepted Evidence via bulk_create.

bulk_create() bypasses Evidence.save(), so if source Evidence.url_type is NULL, the accepted copy stays NULL. Duplicate checking has fallbacks for url_type=NULL, so this isn’t an allow_duplicate-correctness issue, but it does leave accepted Evidence with missing url_type in API/UI data.

🔧 Proposed fix
-from .url_utils import normalize_url
+from .url_utils import detect_url_type, normalize_url
...
-                    url_type=evidence.url_type,
+                    url_type=(
+                        evidence.url_type
+                        or (detect_url_type(evidence.url) if evidence.url else None)
+                    ),
                     normalized_url=normalize_url(evidence.url) if evidence.url else '',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/contributions/views.py` at line 47, When copying accepted Evidence
objects via bulk_create, ensure you backfill url_type for each new Evidence
because bulk_create bypasses Evidence.save(); update the code that prepares the
list for Evidence.objects.bulk_create(...) to set instance.url_type =
source.url_type or, if source.url_type is None, compute and assign it (e.g.,
call normalize_url(source.url) or the existing URL-type helper) so every created
Evidence has a non-null url_type for API/UI usage.
🤖 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 `@backend/poaps/views.py`:
- Around line 360-364: This action exposes another user's POAPs by resolving an
arbitrary `address`; before calling get_object_or_404 you must enforce a
self-or-staff guard: verify that request.user either is staff
(request.user.is_staff or request.user.is_superuser) or that the resolved
account/wallet belongs to request.user (compare request.user.wallet_address /
account.id / username as appropriate), and return a 403/raise PermissionDenied
if neither condition holds; alternatively apply an explicit permission check
(e.g., a custom IsOwnerOrStaff permission) at the start of the viewset action to
protect the address-based lookup in this action.

---

Outside diff comments:
In `@backend/contributions/models.py`:
- Around line 749-757: The save method in the Contribution model leaves a stale
url_type when self.url is empty; update the else branch in save to clear the URL
classification by setting self.url_type = None and self.url_type_id = None (and
ensure self.normalized_url = '' remains), so removal of a URL resets both the
foreign-key object and its id; reference the save method and the fields/methods
url, url_type, url_type_id, normalized_url, detect_url_type, normalize_url when
making this change.

In `@backend/contributions/views.py`:
- Line 47: When copying accepted Evidence objects via bulk_create, ensure you
backfill url_type for each new Evidence because bulk_create bypasses
Evidence.save(); update the code that prepares the list for
Evidence.objects.bulk_create(...) to set instance.url_type = source.url_type or,
if source.url_type is None, compute and assign it (e.g., call
normalize_url(source.url) or the existing URL-type helper) so every created
Evidence has a non-null url_type for API/UI usage.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 91cd8d1c-de08-4797-ad7b-a5f53e38815f

📥 Commits

Reviewing files that changed from the base of the PR and between a9649e5 and f88fa48.

📒 Files selected for processing (7)
  • backend/contributions/models.py
  • backend/contributions/tests/test_steward_permissions.py
  • backend/contributions/views.py
  • backend/ethereum_auth/views.py
  • backend/poaps/tests/test_poaps.py
  • backend/poaps/views.py
  • backend/validators/views.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
backend/contributions/models.py (1)

749-757: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear stale url_type when the URL is removed.

The new auto-detection never resets url_type in the else branch, so an evidence row can become description-only while still carrying its previous URL classification.

🔧 Proposed fix
         else:
+            self.url_type = None
             self.normalized_url = ''
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/contributions/models.py` around lines 749 - 757, The save method in
the Contribution model leaves a stale url_type when self.url is empty; update
the else branch in save to clear the URL classification by setting self.url_type
= None and self.url_type_id = None (and ensure self.normalized_url = ''
remains), so removal of a URL resets both the foreign-key object and its id;
reference the save method and the fields/methods url, url_type, url_type_id,
normalized_url, detect_url_type, normalize_url when making this change.
backend/contributions/views.py (1)

47-47: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Backfill url_type when copying accepted Evidence via bulk_create.

bulk_create() bypasses Evidence.save(), so if source Evidence.url_type is NULL, the accepted copy stays NULL. Duplicate checking has fallbacks for url_type=NULL, so this isn’t an allow_duplicate-correctness issue, but it does leave accepted Evidence with missing url_type in API/UI data.

🔧 Proposed fix
-from .url_utils import normalize_url
+from .url_utils import detect_url_type, normalize_url
...
-                    url_type=evidence.url_type,
+                    url_type=(
+                        evidence.url_type
+                        or (detect_url_type(evidence.url) if evidence.url else None)
+                    ),
                     normalized_url=normalize_url(evidence.url) if evidence.url else '',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/contributions/views.py` at line 47, When copying accepted Evidence
objects via bulk_create, ensure you backfill url_type for each new Evidence
because bulk_create bypasses Evidence.save(); update the code that prepares the
list for Evidence.objects.bulk_create(...) to set instance.url_type =
source.url_type or, if source.url_type is None, compute and assign it (e.g.,
call normalize_url(source.url) or the existing URL-type helper) so every created
Evidence has a non-null url_type for API/UI usage.
🤖 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 `@backend/poaps/views.py`:
- Around line 360-364: This action exposes another user's POAPs by resolving an
arbitrary `address`; before calling get_object_or_404 you must enforce a
self-or-staff guard: verify that request.user either is staff
(request.user.is_staff or request.user.is_superuser) or that the resolved
account/wallet belongs to request.user (compare request.user.wallet_address /
account.id / username as appropriate), and return a 403/raise PermissionDenied
if neither condition holds; alternatively apply an explicit permission check
(e.g., a custom IsOwnerOrStaff permission) at the start of the viewset action to
protect the address-based lookup in this action.

---

Outside diff comments:
In `@backend/contributions/models.py`:
- Around line 749-757: The save method in the Contribution model leaves a stale
url_type when self.url is empty; update the else branch in save to clear the URL
classification by setting self.url_type = None and self.url_type_id = None (and
ensure self.normalized_url = '' remains), so removal of a URL resets both the
foreign-key object and its id; reference the save method and the fields/methods
url, url_type, url_type_id, normalized_url, detect_url_type, normalize_url when
making this change.

In `@backend/contributions/views.py`:
- Line 47: When copying accepted Evidence objects via bulk_create, ensure you
backfill url_type for each new Evidence because bulk_create bypasses
Evidence.save(); update the code that prepares the list for
Evidence.objects.bulk_create(...) to set instance.url_type = source.url_type or,
if source.url_type is None, compute and assign it (e.g., call
normalize_url(source.url) or the existing URL-type helper) so every created
Evidence has a non-null url_type for API/UI usage.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 91cd8d1c-de08-4797-ad7b-a5f53e38815f

📥 Commits

Reviewing files that changed from the base of the PR and between a9649e5 and f88fa48.

📒 Files selected for processing (7)
  • backend/contributions/models.py
  • backend/contributions/tests/test_steward_permissions.py
  • backend/contributions/views.py
  • backend/ethereum_auth/views.py
  • backend/poaps/tests/test_poaps.py
  • backend/poaps/views.py
  • backend/validators/views.py
🛑 Comments failed to post (1)
backend/poaps/views.py (1)

360-364: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce self-or-staff access on this address-based POAP route.

This action still resolves an arbitrary address and returns that account’s POAP history, so any authenticated caller can enumerate another user’s claims by wallet. Authentication alone does not close the IDOR here; gate the action to the owner of the address or staff before get_object_or_404().

🔒 Suggested guard
 def poaps(self, request, address=None):
     from django.shortcuts import get_object_or_404
     from users.models import User

+    if (
+        not request.user.is_staff
+        and normalize_wallet_address(address) != normalize_wallet_address(request.user.address)
+    ):
+        return Response({'detail': 'Not found.'}, status=status.HTTP_404_NOT_FOUND)
+
     user = get_object_or_404(User, address__iexact=address)

As per coding guidelines, backend/{...}/views.py: “Flag any handler/viewset/action that fetches a resource by id, slug, address, wallet, username, path param, or query param without proving object ownership, steward/admin role, or an explicit permission check on that exact resource.”

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

In `@backend/poaps/views.py` around lines 360 - 364, This action exposes another
user's POAPs by resolving an arbitrary `address`; before calling
get_object_or_404 you must enforce a self-or-staff guard: verify that
request.user either is staff (request.user.is_staff or
request.user.is_superuser) or that the resolved account/wallet belongs to
request.user (compare request.user.wallet_address / account.id / username as
appropriate), and return a 403/raise PermissionDenied if neither condition
holds; alternatively apply an explicit permission check (e.g., a custom
IsOwnerOrStaff permission) at the start of the viewset action to protect the
address-based lookup in this action.

@JoaquinBN JoaquinBN merged commit a89b37d into dev Jun 5, 2026
1 check passed
@JoaquinBN JoaquinBN deleted the JoaquinBN/review-security-findings branch June 5, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant