Skip to content

Commit

Permalink
fix: security issues with redirect.
Browse files Browse the repository at this point in the history
  • Loading branch information
apoorvapendse committed Nov 30, 2024
1 parent d11a5b1 commit 9fc7a3a
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 43 deletions.
109 changes: 67 additions & 42 deletions company/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
import uuid
from datetime import datetime, timedelta
from urllib.parse import parse_qs, urlparse
from urllib.parse import parse_qs, urlencode, urlparse

import requests
from django.contrib import messages
Expand All @@ -15,6 +15,7 @@
from django.db.models.functions import ExtractMonth
from django.http import Http404, HttpResponseBadRequest, HttpResponseServerError, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.http import require_http_methods
from django.views.generic import View
Expand Down Expand Up @@ -762,11 +763,11 @@ def get(self, request, id, *args, **kwargs):
)

if slack_integration:
# If Slack integration exists, show the details
bot_token = slack_integration.bot_access_token
app = App(token=bot_token)
channels_list = self.get_channel_names(app)
hours = range(0, 24)

hours = range(24)
return render(
request,
"company/add_slack_integration.html",
Expand All @@ -778,27 +779,42 @@ def get(self, request, id, *args, **kwargs):
},
)

# If no Slack integration found, proceed to OAuth flow
# Redirect to Slack OAuth flow if no integration exists
client_id = os.getenv("SLACK_CLIENT_ID")
scopes = "channels:read,chat:write,groups:read,channels:join"
state = f"company_id={id}"
redirect_uri = "http://localhost:8000/oauth/slack/callback"
state = urlencode({"company_id": id})
allowed_redirect_uris = [
"http://localhost:8000/oauth/slack/callback",

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.
"https://your-production-url.com/oauth/slack/callback",
]

if redirect_uri not in allowed_redirect_uris:
raise ValueError("Invalid redirect URI")

state = urlencode({"company_id": id})

auth_url = (
f"https://slack.com/oauth/v2/authorize?client_id={client_id}"
f"&scope={scopes}&state={state}&redirect_uri=http://localhost:8000/oauth/slack/callback"
f"https://slack.com/oauth/v2/authorize"
f"?client_id={client_id}&scope=channels:read,chat:write,groups:read,channels:join"
f"&state={state}&redirect_uri={redirect_uri}"
)
return redirect(auth_url)

def get_channel_names(self, app):
"""Fetches channel names from Slack."""
cursor = None
channels = []
while True:
response = app.client.conversations_list(cursor=cursor)
if response["ok"]:
for channel in response["channels"]:
channels.append(channel["name"])
cursor = response.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
try:
while True:
response = app.client.conversations_list(cursor=cursor)
if response["ok"]:
channels.extend(channel["name"] for channel in response["channels"])
cursor = response.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
except Exception as e:
print("Error fetching channels", e)
return channels

@validate_company_user
Expand All @@ -807,11 +823,10 @@ def post(self, request, id, *args, **kwargs):
return self.delete(request, id, *args, **kwargs)

slack_data = {
"default_channel": request.POST.get("target_channel", None),
"daily_sizzle_timelogs_status": request.POST.get("daily_sizzle_timelogs_status", None),
"daily_sizzle_timelogs_hour": request.POST.get("daily_sizzle_timelogs_hour", None),
"default_channel": request.POST.get("target_channel"),
"daily_sizzle_timelogs_status": request.POST.get("daily_sizzle_timelogs_status"),
"daily_sizzle_timelogs_hour": request.POST.get("daily_sizzle_timelogs_hour"),
}
channel_id = None
slack_integration = (
SlackIntegration.objects.filter(
integration__company_id=id,
Expand All @@ -824,33 +839,35 @@ def post(self, request, id, *args, **kwargs):
if slack_integration:
app = App(token=slack_integration.bot_access_token)
if slack_data["default_channel"]:
channel_id = self.get_channel_id(app, slack_data["default_channel"])
slack_integration.default_channel_id = channel_id
slack_integration.default_channel_id = self.get_channel_id(
app, slack_data["default_channel"]
)
slack_integration.default_channel_name = slack_data["default_channel"]
if slack_data["daily_sizzle_timelogs_status"]:
slack_integration.daily_updates = True
if slack_data["daily_sizzle_timelogs_hour"]:
slack_integration.daily_update_time = slack_data["daily_sizzle_timelogs_hour"]
slack_integration.daily_updates = bool(slack_data["daily_sizzle_timelogs_status"])
slack_integration.daily_update_time = slack_data["daily_sizzle_timelogs_hour"]
slack_integration.save()

return redirect("company_manage_integrations", id=id)

def get_channel_id(self, app, channel_name):
"""Fetches a Slack channel ID by name."""
cursor = None
while True:
response = app.client.conversations_list(cursor=cursor)
for channel in response["channels"]:
if channel["name"] == channel_name.strip("#"):
return channel["id"]
cursor = response.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
try:
while True:
response = app.conversations_list(cursor=cursor)
for channel in response["channels"]:
if channel["name"] == channel_name.strip("#"):
return channel["id"]
cursor = response.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
except Exception as e:
print("Error fetching channel ID:", e)
return None

@validate_company_user
def delete(self, request, id, *args, **kwargs):
"""
Deletes the Slack integration for the given company ID.
"""
"""Deletes the Slack integration."""
slack_integration = (
SlackIntegration.objects.filter(
integration__company_id=id,
Expand All @@ -862,7 +879,6 @@ def delete(self, request, id, *args, **kwargs):

if slack_integration:
slack_integration.delete()
return redirect("company_manage_integrations", id=id)

return redirect("company_manage_integrations", id=id)

Expand All @@ -879,8 +895,13 @@ def get(self, request, *args, **kwargs):
return HttpResponseBadRequest("Invalid or missing parameters")

try:
access_token = self.exchange_code_for_token(code, company_id)
app = App(token=access_token)
if not company_id.isdigit():
return HttpResponseBadRequest("Invalid company ID")

company_id = int(company_id) # Safely cast to int after validation

# Exchange code for token
access_token = self.exchange_code_for_token(code)

integration = Integration.objects.create(
company_id=company_id,
Expand All @@ -890,12 +911,16 @@ def get(self, request, *args, **kwargs):
integration=integration,
bot_access_token=access_token,
)
return redirect(f"/company/{company_id}/dashboard/integrations")

dashboard_url = reverse("company_manage_integrations", args=[company_id])
return redirect(dashboard_url)

except Exception as e:
print(f"Error while exchanging code: {e}")
print(f"Error during Slack OAuth callback: {e}")
return HttpResponseServerError("An error occurred")

def exchange_code_for_token(self, code, company_id):
def exchange_code_for_token(self, code):
"""Exchanges OAuth code for Slack access token."""
client_id = os.getenv("SLACK_CLIENT_ID")
client_secret = os.getenv("SLACK_CLIENT_SECRET")
redirect_uri = "http://localhost:8000/oauth/slack/callback"
Expand Down
2 changes: 1 addition & 1 deletion website/tests_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def setUp(self):

def test_responses(
self,
allowed_http_codes=[200, 302, 405, 401, 404],
allowed_http_codes=[200, 302, 405, 401, 404, 400],
credentials={},
default_kwargs={},
):
Expand Down

0 comments on commit 9fc7a3a

Please sign in to comment.