This commit addresses multiple CRITICAL, HIGH, and MEDIUM severity security vulnerabilities identified in the security audit: CRITICAL FIXES: - Fix weak RNG for recovery codes - replaced Enum.random() with :crypto.strong_rand_bytes/1 for cryptographically secure token generation - Fix subscription limit race conditions - moved free org and device quota checks inside transactions with FOR UPDATE locks to prevent concurrent bypass - Fix default organization race condition - moved is_default check inside transaction to prevent multiple defaults per user HIGH SEVERITY FIXES: - Fix agent token deletion race condition - moved PubSub broadcast inside transaction to ensure agents only receive notification after successful deletion MEDIUM SEVERITY FIXES: - Fix LIKE wildcard injection in search - applied sanitize_like() to all user-facing search queries in devices.ex, sites.ex, and gaiia.ex to prevent enumeration attacks - Fix Jason.decode! DoS - replaced with safe Jason.decode/1 with error handling in device_live/index.ex - Fix SSRF vulnerability - added URL validation in HTTP executor to block requests to private/internal IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16) - Fix error information leakage - replaced inspect() in API responses with generic error messages, logging details server-side only - Fix atom table pollution - HTTP method normalization now uses whitelist mapping instead of String.to_atom() SECURITY IMPROVEMENTS: - All quota checks now use pessimistic locking (SELECT FOR UPDATE) to prevent TOCTOU race conditions - Private IP validation prevents cloud metadata service access (169.254.169.254) - DNS resolution performed before HTTP requests to detect IP spoofing - Error details logged server-side but not exposed to clients Files changed: - lib/towerops/accounts/user_recovery_code.ex - lib/towerops/organizations.ex - lib/towerops/devices.ex - lib/towerops/sites.ex - lib/towerops/gaiia.ex - lib/towerops/agents.ex - lib/towerops/monitoring/executors/http_executor.ex - lib/towerops_web/live/device_live/index.ex - lib/towerops_web/controllers/api/v1/mib_controller.ex - lib/towerops_web/controllers/api/v1/agent_release_webhook_controller.ex - lib/towerops_web/controllers/api/v1/geoip_controller.ex Reviewed-on: graham/towerops-web#108
242 lines
7.1 KiB
Elixir
242 lines
7.1 KiB
Elixir
defmodule ToweropsWeb.UserSessionController do
|
|
@moduledoc false
|
|
|
|
use ToweropsWeb, :controller
|
|
|
|
alias Towerops.Accounts
|
|
alias ToweropsWeb.UserAuth
|
|
|
|
def new(conn, params) do
|
|
# Check for email in query params (from registration redirect) or current user
|
|
email = params["email"] || get_in(conn.assigns, [:current_scope, Access.key(:user), Access.key(:email)])
|
|
form = Phoenix.Component.to_form(%{"email" => email}, as: "user")
|
|
|
|
render(conn, :new, form: form)
|
|
end
|
|
|
|
# magic link login
|
|
def create(conn, %{"user" => %{"token" => token} = user_params} = params) do
|
|
case Accounts.login_user_by_magic_link(token) do
|
|
{:ok, {user, _expired_tokens}} ->
|
|
conn =
|
|
if params["_action"] == "confirmed" do
|
|
put_flash(conn, :info, t("User confirmed successfully."))
|
|
else
|
|
conn
|
|
end
|
|
|
|
UserAuth.log_in_user(conn, user, user_params)
|
|
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_flash(:error, t("The link is invalid or it has expired."))
|
|
|> render(:new, form: Phoenix.Component.to_form(%{}, as: "user"))
|
|
end
|
|
end
|
|
|
|
# email + password login
|
|
def create(conn, %{"user" => %{"email" => email, "password" => password} = user_params}) do
|
|
if user = Accounts.get_user_by_email_and_password(email, password) do
|
|
cond do
|
|
is_nil(user.confirmed_at) ->
|
|
conn
|
|
|> put_flash(
|
|
:error,
|
|
"Please verify your email address before logging in. Check your inbox for a confirmation link."
|
|
)
|
|
|> render(:new, form: Phoenix.Component.to_form(user_params, as: "user"))
|
|
|
|
# Check if user has TOTP enabled - require verification before creating session
|
|
Accounts.totp_enabled?(user) ->
|
|
remember_me = Map.get(user_params, "remember_me") == "true"
|
|
|
|
conn
|
|
|> put_session(:pending_totp_user_id, user.id)
|
|
|> put_session(:pending_totp_remember_me, remember_me)
|
|
|> redirect(to: ~p"/users/log-in/totp")
|
|
|
|
true ->
|
|
# No TOTP, proceed with normal login
|
|
UserAuth.log_in_user(conn, user, user_params)
|
|
end
|
|
else
|
|
# Record failed login attempt for security monitoring
|
|
_ =
|
|
Accounts.record_login_attempt(%{
|
|
email: email,
|
|
success: false,
|
|
failure_reason: "invalid_credentials",
|
|
method: "password",
|
|
ip_address: ToweropsWeb.RemoteIp.from_conn(conn),
|
|
user_agent: extract_user_agent(conn)
|
|
})
|
|
|
|
form = Phoenix.Component.to_form(user_params, as: "user")
|
|
|
|
# In order to prevent user enumeration attacks, don't disclose whether the email is registered.
|
|
conn
|
|
|> put_flash(:error, t("Invalid email or password"))
|
|
|> render(:new, form: form)
|
|
end
|
|
end
|
|
|
|
# magic link request
|
|
def create(conn, %{"user" => %{"email" => email}}) do
|
|
_ =
|
|
if user = Accounts.get_user_by_email(email) do
|
|
Accounts.deliver_login_instructions(
|
|
user,
|
|
&url(~p"/users/log-in/#{&1}")
|
|
)
|
|
end
|
|
|
|
info =
|
|
"If your email is in our system, you will receive instructions for logging in shortly."
|
|
|
|
conn
|
|
|> put_flash(:info, info)
|
|
|> redirect(to: ~p"/users/log-in")
|
|
end
|
|
|
|
def confirm(conn, %{"token" => token}) do
|
|
# Check if user was previously confirmed before logging in
|
|
was_confirmed =
|
|
case Accounts.get_user_by_magic_link_token(token) do
|
|
%Accounts.User{confirmed_at: confirmed_at} when not is_nil(confirmed_at) -> true
|
|
_ -> false
|
|
end
|
|
|
|
case Accounts.login_user_by_magic_link(token) do
|
|
{:ok, {user, _expired_tokens}} ->
|
|
conn =
|
|
if was_confirmed do
|
|
conn
|
|
else
|
|
put_flash(conn, :info, t("User confirmed successfully."))
|
|
end
|
|
|
|
# Always use remember_me: true to keep user logged in
|
|
UserAuth.log_in_user(conn, user, %{"remember_me" => "true"})
|
|
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_flash(:error, t("Magic link is invalid or it has expired."))
|
|
|> redirect(to: ~p"/users/log-in")
|
|
end
|
|
end
|
|
|
|
def switch_org(conn, %{"org_id" => org_id}) do
|
|
user = conn.assigns.current_scope.user
|
|
membership = Towerops.Organizations.get_membership(org_id, user.id)
|
|
|
|
if membership do
|
|
{:ok, _user} = Accounts.update_user_profile(user, %{default_organization_id: org_id})
|
|
|
|
conn
|
|
|> put_session(:current_organization_id, org_id)
|
|
|> redirect(to: ~p"/dashboard")
|
|
else
|
|
conn
|
|
|> put_flash(:error, t("You don't have access to this organization."))
|
|
|> redirect(to: ~p"/orgs")
|
|
end
|
|
end
|
|
|
|
def delete(conn, _params) do
|
|
conn
|
|
|> put_flash(:info, t("Logged out successfully."))
|
|
|> UserAuth.log_out_user()
|
|
end
|
|
|
|
def totp_verification_form(conn, _params) do
|
|
case get_session(conn, :pending_totp_user_id) do
|
|
nil ->
|
|
conn
|
|
|> put_flash(:error, t("Please log in first."))
|
|
|> redirect(to: ~p"/users/log-in")
|
|
|
|
_user_id ->
|
|
form = Phoenix.Component.to_form(%{}, as: "user")
|
|
render(conn, :totp, form: form)
|
|
end
|
|
end
|
|
|
|
def verify_totp(conn, %{"user" => %{"totp_code" => code}}) do
|
|
case get_session(conn, :pending_totp_user_id) do
|
|
nil ->
|
|
redirect_to_login(conn)
|
|
|
|
user_id ->
|
|
handle_totp_verification(conn, user_id, code)
|
|
end
|
|
end
|
|
|
|
defp redirect_to_login(conn) do
|
|
conn
|
|
|> put_flash(:error, t("Please log in first."))
|
|
|> redirect(to: ~p"/users/log-in")
|
|
end
|
|
|
|
defp handle_totp_verification(conn, user_id, code) do
|
|
user = Accounts.get_user!(user_id)
|
|
|
|
case Accounts.verify_user_mfa(user, code) do
|
|
{:ok, user, :totp} ->
|
|
complete_totp_login(conn, user)
|
|
|
|
{:ok, user, :recovery_code} ->
|
|
conn
|
|
|> put_flash(
|
|
:warning,
|
|
"You used a recovery code. Consider regenerating codes from your account settings."
|
|
)
|
|
|> complete_totp_login(user)
|
|
|
|
{:error, reason} ->
|
|
# Record failed TOTP verification for security monitoring
|
|
_ =
|
|
Accounts.record_login_attempt(%{
|
|
email: user.email,
|
|
success: false,
|
|
failure_reason: "invalid_totp",
|
|
method: "totp",
|
|
ip_address: ToweropsWeb.RemoteIp.from_conn(conn),
|
|
user_agent: extract_user_agent(conn),
|
|
metadata: %{reason: inspect(reason)}
|
|
})
|
|
|
|
render_totp_error(conn, code)
|
|
end
|
|
end
|
|
|
|
defp complete_totp_login(conn, user) do
|
|
remember_me = get_session(conn, :pending_totp_remember_me)
|
|
|
|
user_params =
|
|
if remember_me do
|
|
%{"remember_me" => "true"}
|
|
else
|
|
%{}
|
|
end
|
|
|
|
conn
|
|
|> delete_session(:pending_totp_user_id)
|
|
|> delete_session(:pending_totp_remember_me)
|
|
|> UserAuth.log_in_user(user, user_params)
|
|
end
|
|
|
|
defp render_totp_error(conn, code) do
|
|
form = Phoenix.Component.to_form(%{"totp_code" => code}, as: "user")
|
|
|
|
conn
|
|
|> put_flash(:error, t("Invalid authentication code. Please try again."))
|
|
|> render(:totp, form: form)
|
|
end
|
|
|
|
defp extract_user_agent(conn) do
|
|
case Plug.Conn.get_req_header(conn, "user-agent") do
|
|
[ua | _] -> ua
|
|
[] -> nil
|
|
end
|
|
end
|
|
end
|