Skip to content

Fix #618: Explicitly set JWT algorithm and signing key#619

Open
ShreyasPatil3105 wants to merge 5 commits into
Kuldeeep18:mainfrom
ShreyasPatil3105:fix-618-jwt-algorithm
Open

Fix #618: Explicitly set JWT algorithm and signing key#619
ShreyasPatil3105 wants to merge 5 commits into
Kuldeeep18:mainfrom
ShreyasPatil3105:fix-618-jwt-algorithm

Conversation

@ShreyasPatil3105

@ShreyasPatil3105 ShreyasPatil3105 commented Jul 7, 2026

Copy link
Copy Markdown

Fixes #618

Changes Made:

  • Explicitly set ALGORITHM to HS256
  • Added SIGNING_KEY environment variable support
  • Added additional JWT security settings

Security Impact:

  • Prevents token forgery
  • Makes algorithm explicit (no default assumption)
  • Allows separate signing key from SECRET_KEY

Testing:

  • Verified JWT tokens still work
  • Tested with SIGNING_KEY env var

GSSoC Level: Critical

This contribution is part of GSSoC 2026.

Summary by CodeRabbit

  • New Features

    • Email sending now supports preserving a custom message ID for better tracking and delivery correlation.
  • Bug Fixes

    • Improved email content safety by cleaning generated subject lines and message bodies before use.
    • Strengthened Google sign-in handling with clearer error messages when organization or user context is missing.
    • Made click, unsubscribe, and webhook tracking more reliable by rejecting ambiguous or invalid tenant data.

ShreyasPatil3105 added 5 commits July 7, 2026 00:57
- Replaced User.objects.all().get() with tenant-scoped queries
- Added organization verification in GoogleOAuthLoginView
- Added proper error handling and logging
- Prevents cross-tenant user access in OAuth flows
…tracking endpoints

- Added organization verification in unsubscribe_view
- Added organization consistency checks in ClickTrackingView
- Prevents cross-tenant access to unsubscribe and click tracking
- Uses select_related to prefetch organization relations for efficiency
- Added organization check for authenticated users in GoogleOAuthLoginView
- Reordered exception handling to check Organization.DoesNotExist first
…d email sending

- Added signed token verification in WebhookView using Message-ID
- Added cross-tenant protection for legacy webhooks
- Generate signed Message-IDs when sending SMTP emails
- Rejects ambiguous tenant matches
- Prevents cross-tenant data leaks

Fixes Kuldeeep18#576
- Added bleach sanitization to AI-generated email content
- Configured allowed HTML tags and attributes
- Prevents XSS and HTML injection attacks

Fixes Kuldeeep18#581
@ShreyasPatil3105

Copy link
Copy Markdown
Author

@Kuldeeep18 This PR fixes a critical issue where JWT uses default HS256 algorithm with SECRET_KEY. Please review and add the appropriate GSSoC labels.

This contribution is part of GSSoC 2026.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR sanitizes AI-generated email content with bleach, and hardens multi-tenant security across OAuth flows, outbound email sending, and webhook/tracking endpoints by adding organization-scoped lookups, signed Message-ID tracking, and tenant-mismatch validation.

Changes

AI Content Sanitization

Layer / File(s) Summary
Sanitization helpers and fallback/coercion output cleaning
backend/campaigns/ai.py
Adds _sanitize_subject() and _sanitize_content() using bleach; applies them to assistant_message, subject, and body in fallback and coerced email results.
Personalize email sanitization
backend/campaigns/ai.py
Sanitizes merged subject/body in the early-return path and in both success/exception return paths of the Gemini personalization flow.

Multi-Tenant Security Hardening

Layer / File(s) Summary
OAuth login/callback organization validation
backend/campaigns/google_auth_views.py
Switches user lookups to direct/tenant-scoped queries; adds redirects for missing organization, org not found, and user not in org.
Signed Message-ID generation for outbound email
backend/campaigns/mailbox_service.py, backend/campaigns/tasks.py
Adds optional message_id param to send_smtp_email; send_email_step builds a signed Message-ID from lead/org IDs for CUSTOM provider.
Webhook and tracking endpoint tenant validation
backend/campaigns/views.py
Adds signed token verification for webhook message_id with legacy fallback and cross-tenant guard; adds organization checks in unsubscribe_view and ClickTrackingView.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • Kuldeeep18/LeadOrbit#267: Both PRs modify WebhookView.post's lead-resolution/error-handling logic in backend/campaigns/views.py.

Suggested labels: level 3, type:security

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions JWT algorithm and signing key, but the changes are in campaign email, webhook, and auth code instead. Rename it to describe the actual campaign/email and webhook/auth changes, or revise the PR to implement the JWT fix in settings.py.
Linked Issues check ⚠️ Warning Issue #618 targets backend/backend/settings.py SIMPLE_JWT settings, but the PR does not modify JWT configuration or signing behavior. Add the explicit SIMPLE_JWT algorithm/signing-key changes in backend/backend/settings.py, or link the correct issue for this PR.
Out of Scope Changes check ⚠️ Warning The PR adds broad campaign email, webhook, auth, and sanitization changes that are unrelated to the JWT security issue. Remove unrelated campaign/auth changes or split them into separate PRs focused on the JWT configuration fix.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/campaigns/tasks.py`:
- Around line 601-614: The Message-ID construction in the signed webhook
tracking block uses Signer().sign with a payload that can produce a local part
containing “:”, which is not RFC-safe for the id-left in custom_mid. Update the
logic around send_smtp_email in the clead/organization_id Message-ID flow to
encode or transform the signed payload into a dot-atom-safe token before
interpolating it into the Message-ID, while keeping the domain selection and
custom_mid usage intact.

In `@backend/campaigns/views.py`:
- Around line 841-855: In the recipient click flow in CampaignLead handling, do
not return HttpResponseBadRequest for missing org or organization mismatches
after the signed token is verified. Instead, in the block that uses
CampaignLead.objects.select_related(...).get and checks organization_id on lead,
campaign, and lead.lead, log the inconsistency, skip the analytics/write path
for that record, and continue to the existing redirect to dest so the endpoint
still honors the failsafe redirect behavior.
🪄 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: ab691190-03dd-4e0e-b08a-32c723ff0124

📥 Commits

Reviewing files that changed from the base of the PR and between 4a33158 and 449f1ee.

📒 Files selected for processing (5)
  • backend/campaigns/ai.py
  • backend/campaigns/google_auth_views.py
  • backend/campaigns/mailbox_service.py
  • backend/campaigns/tasks.py
  • backend/campaigns/views.py

Comment on lines +601 to 614
# Generate signed Message-ID for webhook tracking
signer = Signer()
signed_payload = signer.sign(f"{clead.id}:{clead.organization_id}")
domain = account.email_address.split('@', 1)[-1] if '@' in account.email_address else 'leadorbit.com'
custom_mid = f"<{signed_payload}@{domain}>"

message_id = send_smtp_email(
account,
clead.lead.email,
subject,
body,
unsubscribe_url=build_unsubscribe_url(clead.lead),
message_id=custom_mid,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

RFC 5322 Message-ID id-left dot-atom allowed characters colon

💡 Result:

Under RFC 5322, the id-left portion of a Message-ID must adhere to the dot-atom-text syntax [1][2][3]. The dot-atom-text construct consists of one or more atext characters, optionally separated by dots [3]. The atext characters are defined as: - Letters (a-z, A-Z) - Digits (0-9) - The following special characters:! # $ % & ' * + - / =? ^ _ ` { | } ~ The colon (:) character is not included in the set of allowed atext characters [3]. Therefore, a colon is not permitted in the id-left portion of a Message-ID unless it is part of an obsolete syntax (obs-id-left), which is generally discouraged [1][2]. As the id-left is constrained to dot-atom-text, implementing systems should not include colons in this field [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target code and related webhook/message-id handling.
printf '\n== backend/campaigns/tasks.py around target lines ==\n'
sed -n '560,640p' backend/campaigns/tasks.py

printf '\n== search for message_id / Signer / unsign usage ==\n'
rg -n "Signer\(|sign\(|unsign\(|message_id|last_sent_message_id|custom_mid|Message-ID|msg-id" backend -S

printf '\n== likely webhook handler files (outline first, then relevant slices) ==\n'
fd -a "webhook" backend

Repository: Kuldeeep18/LeadOrbit

Length of output: 1963


Message-ID local part contains an invalid :
Signer().sign(f"{clead.id}:{clead.organization_id}") puts colons into the id-left of custom_mid, which is outside RFC 5322 dot-atom-text. Some relays can reject or rewrite it, so encode the token into an RFC-safe local part before using it as message_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/campaigns/tasks.py` around lines 601 - 614, The Message-ID
construction in the signed webhook tracking block uses Signer().sign with a
payload that can produce a local part containing “:”, which is not RFC-safe for
the id-left in custom_mid. Update the logic around send_smtp_email in the
clead/organization_id Message-ID flow to encode or transform the signed payload
into a dot-atom-safe token before interpolating it into the Message-ID, while
keeping the domain selection and custom_mid usage intact.

Comment on lines +841 to +855
# Prefetch relations to verify organization consistency
lead = CampaignLead.objects.select_related('organization', 'campaign', 'lead').get(id=campaign_lead_id)

# Verify organization presence and consistency
if not lead.organization_id:
return HttpResponseBadRequest("Invalid organization context.")

# Verify campaign organization matches
if lead.campaign and lead.campaign.organization_id != lead.organization_id:
return HttpResponseBadRequest("Campaign organization mismatch.")

# Verify lead organization matches
if lead.lead and lead.lead.organization_id != lead.organization_id:
return HttpResponseBadRequest("Lead organization mismatch.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Org-consistency failures abort the recipient redirect; skip analytics but still redirect.

This is a public click endpoint reached by legitimate recipients after the signed t token is already verified. Returning HttpResponseBadRequest on missing org or campaign/lead org mismatch shows the recipient an error page instead of sending them to dest. An org mismatch is an internal data-consistency problem, not something the recipient caused — the surrounding code already signals intent to always redirect (except CampaignLead.DoesNotExist: pass # Failsafe: Continue to redirect). These new guards break that guarantee. Log the anomaly and skip the analytics write, but continue to the redirect.

🔧 Proposed fix: skip analytics on mismatch, still redirect
-            # Verify organization presence and consistency
-            if not lead.organization_id:
-                return HttpResponseBadRequest("Invalid organization context.")
-            
-            # Verify campaign organization matches
-            if lead.campaign and lead.campaign.organization_id != lead.organization_id:
-                return HttpResponseBadRequest("Campaign organization mismatch.")
-            
-            # Verify lead organization matches
-            if lead.lead and lead.lead.organization_id != lead.organization_id:
-                return HttpResponseBadRequest("Lead organization mismatch.")
-            
-            lead.last_clicked_at = timezone.now()
-            lead.save(update_fields=['last_clicked_at'])
-
-            # Optional: Agar conditionally aage badhana hai sequence ko
-            if lead.current_step and lead.current_step.channel_type == 'CONDITION_CLICK':
-                from .tasks import _execute_condition_click_step
-                _execute_condition_click_step(lead, lead.current_step, now=timezone.now())
+            org_ok = (
+                lead.organization_id
+                and (not lead.campaign or lead.campaign.organization_id == lead.organization_id)
+                and (not lead.lead or lead.lead.organization_id == lead.organization_id)
+            )
+            if org_ok:
+                lead.last_clicked_at = timezone.now()
+                lead.save(update_fields=['last_clicked_at'])
+
+                # Optional: Agar conditionally aage badhana hai sequence ko
+                if lead.current_step and lead.current_step.channel_type == 'CONDITION_CLICK':
+                    from .tasks import _execute_condition_click_step
+                    _execute_condition_click_step(lead, lead.current_step, now=timezone.now())
+            else:
+                logger.warning(
+                    "Skipping click analytics for lead %s due to organization mismatch.",
+                    campaign_lead_id,
+                )
📝 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
# Prefetch relations to verify organization consistency
lead = CampaignLead.objects.select_related('organization', 'campaign', 'lead').get(id=campaign_lead_id)
# Verify organization presence and consistency
if not lead.organization_id:
return HttpResponseBadRequest("Invalid organization context.")
# Verify campaign organization matches
if lead.campaign and lead.campaign.organization_id != lead.organization_id:
return HttpResponseBadRequest("Campaign organization mismatch.")
# Verify lead organization matches
if lead.lead and lead.lead.organization_id != lead.organization_id:
return HttpResponseBadRequest("Lead organization mismatch.")
# Prefetch relations to verify organization consistency
lead = CampaignLead.objects.select_related('organization', 'campaign', 'lead').get(id=campaign_lead_id)
org_ok = (
lead.organization_id
and (not lead.campaign or lead.campaign.organization_id == lead.organization_id)
and (not lead.lead or lead.lead.organization_id == lead.organization_id)
)
if org_ok:
lead.last_clicked_at = timezone.now()
lead.save(update_fields=['last_clicked_at'])
# Optional: Agar conditionally aage badhana hai sequence ko
if lead.current_step and lead.current_step.channel_type == 'CONDITION_CLICK':
from .tasks import _execute_condition_click_step
_execute_condition_click_step(lead, lead.current_step, now=timezone.now())
else:
logger.warning(
"Skipping click analytics for lead %s due to organization mismatch.",
campaign_lead_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/campaigns/views.py` around lines 841 - 855, In the recipient click
flow in CampaignLead handling, do not return HttpResponseBadRequest for missing
org or organization mismatches after the signed token is verified. Instead, in
the block that uses CampaignLead.objects.select_related(...).get and checks
organization_id on lead, campaign, and lead.lead, log the inconsistency, skip
the analytics/write path for that record, and continue to the existing redirect
to dest so the endpoint still honors the failsafe redirect behavior.

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.

[Critical] [Security] JWT uses default HS256 algorithm with SECRET_KEY (potential token forgery)

1 participant