Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a8f779d
Add IP-based rate limiting for Copi (fixes #1877)
immortal71 Feb 8, 2026
769d544
Added WebSocket connection rate limiting and security docs
immortal71 Feb 8, 2026
89d2159
Add comprehensive tests to increase coverage above 80%(only got 76.5 …
immortal71 Feb 8, 2026
445b1c7
Update copi.owasp.org/lib/copi_web/endpoint.ex
immortal71 Feb 8, 2026
8f14ab6
Update copi.owasp.org/SECURITY.md
immortal71 Feb 8, 2026
39b7b4a
Update copi.owasp.org/lib/copi/rate_limiter.ex
immortal71 Feb 8, 2026
49f9e56
Address all Copilot feedback and boost coverage above 80%
immortal71 Feb 8, 2026
f250b13
Boost test coverage to 80% and fix test setup issues
immortal71 Feb 8, 2026
f30f5a3
Add comprehensive tests to push coverage above 80%
immortal71 Feb 8, 2026
3f550a6
Fix gettext test import issue
immortal71 Feb 8, 2026
9185c5b
Fix test compilation and assertion errors
immortal71 Feb 8, 2026
162f10d
Fix IPHelper fallback behavior and enhance test coverage - Return loc…
immortal71 Feb 8, 2026
9fcfae0
Fix test failures: struct comparison, rate limit test, and database p…
immortal71 Feb 8, 2026
ef9e991
Fix LiveView test assertions - handle redirects instead of expecting …
immortal71 Feb 8, 2026
2a28e9a
Fix test failures: database ownership, rate limit flash rendering, re…
immortal71 Feb 8, 2026
ad04fd2
Fix remaining test failures: API ULID, card filter reloading, redirec…
immortal71 Feb 8, 2026
6fc9d4b
Fix all 7 test failures: module name, flash capture from render_submi…
immortal71 Feb 8, 2026
676a30a
Fix 6 test failures: verify rate limiting via state not flash HTML, c…
immortal71 Feb 8, 2026
d6d07e9
feat: Address maintainer feedback for rate limiting
immortal71 Feb 16, 2026
5714610
fix: Address bugs found in code review
immortal71 Feb 17, 2026
f73b693
Merge branch 'master' of https://github.com/OWASP/cornucopia into fea…
immortal71 Feb 17, 2026
d238087
fix: Restore removed tests to recover coverage
immortal71 Feb 17, 2026
24a9400
fix: Improve broadcast test to properly verify message handling
immortal71 Feb 17, 2026
c526913
test: Add comprehensive continue voting tests for coverage
immortal71 Feb 17, 2026
1878843
fix: Correct test failures for broadcast and rate limiter
immortal71 Feb 17, 2026
e18812a
refactor: Use Phoenix.LiveView public API for connect_info
immortal71 Feb 18, 2026
f05acf3
fix: Simplify connect_info access with safe pattern matching
immortal71 Feb 18, 2026
bbb3a63
Fix race condition in voting system
immortal71 Feb 19, 2026
6c93cae
Add test coverage for race condition fixes
immortal71 Feb 19, 2026
6a18849
Update copi.owasp.org/lib/copi/rate_limiter.ex
immortal71 Feb 19, 2026
7634545
Update copi.owasp.org/test/copi_web/router_test.exs
immortal71 Feb 19, 2026
447299e
Address Copilot review suggestions
immortal71 Feb 19, 2026
788e801
Merge branch 'fix/vote-race-condition' of https://github.com/immortal…
immortal71 Feb 19, 2026
c800d6b
Fix invalid return syntax in toggle_vote handler
immortal71 Feb 19, 2026
d02539f
Simplify integer parsing and remove unused alias
immortal71 Feb 19, 2026
5d6a850
Fix migration to work in test environment
immortal71 Feb 19, 2026
e880c1a
Fix migration for test compatibility
immortal71 Feb 19, 2026
d533510
Simplify migration - just create unique index without DELETE
immortal71 Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions copi.owasp.org/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Security - Rate Limiting Implementation

## Overview

Copi implements IP-based rate limiting to protect against CAPEC-212 (Functionality Misuse) attacks and ensure service availability under potential abuse scenarios.

## Rate Limiting Strategy

The application uses a GenServer-based rate limiter that tracks requests per IP address across different action types. This prevents malicious actors from overwhelming the service while maintaining usability for legitimate users.

### Protected Actions

1. **Game Creation**: Limited to prevent mass game creation attacks
2. **Player Creation**: Limited to prevent player spam
3. **WebSocket Connections**: Limited to prevent connection flooding

## Default Rate Limits

| Action | Limit | Time Window |
| --- | --- | --- |
| Game Creation | 20 requests | per hour per IP |
| Player Creation | 60 requests | per hour per IP |
| WebSocket Connections | 333 connections | per second per IP |

## Configuration

Rate limits are configurable via environment variables:

```bash
# Game creation limits
RATE_LIMIT_GAME_CREATION_LIMIT=20
RATE_LIMIT_GAME_CREATION_WINDOW=3600 # seconds

# Player creation limits
RATE_LIMIT_PLAYER_CREATION_LIMIT=60
RATE_LIMIT_PLAYER_CREATION_WINDOW=3600 # seconds

# Connection limits
RATE_LIMIT_CONNECTION_LIMIT=333
RATE_LIMIT_CONNECTION_WINDOW=1 # seconds
```

**Note**: Environment variables must be positive integers. Invalid values will log a warning and fall back to defaults.

## Implementation Details

### Architecture

- **RateLimiter GenServer** (`lib/copi/rate_limiter.ex`): Core rate limiting logic
- Tracks requests per IP and action type
- Implements sliding window algorithm
- Auto-cleanup of expired entries every 5 minutes

- **IPHelper Module** (`lib/copi/ip_helper.ex`): IP extraction utilities
- Provides DRY interface for IP extraction
- Handles LiveView sockets, Phoenix sockets, and Plug connections

- **Integration Points**:
- Game creation: `lib/copi_web/live/game_live/create_game_form.ex`
- Player creation: `lib/copi_web/live/player_live/form_component.ex`
- WebSocket connections: `lib/copi_web/channels/user_socket.ex`

### Rate Limit Response

When rate limits are exceeded:

- **Game/Player Creation**: User receives a flash error message: "Too many [action] attempts. Please try again later."
- **WebSocket Connections**: Connection is denied (returns `:error`)
- **Logging**: Rate limit violations are logged for monitoring

### IP Address Handling

- Supports both IPv4 and IPv6
- Accepts IP addresses as tuples or strings
- Normalizes IP formats for consistent tracking
- **Proxy Support**: Automatically reads `X-Forwarded-For` header to get real client IP when behind reverse proxies
- Uses leftmost IP from X-Forwarded-For (original client IP)
- Falls back to `remote_ip` if header is missing or invalid
- Falls back gracefully when IP is unavailable

## Testing

Comprehensive test coverage in:
- `test/copi/rate_limiter_test.exs`: Core rate limiter functionality
- `test/copi_web/channels/user_socket_test.exs`: WebSocket connection rate limiting

## Security Considerations

### Limitations

1. **IP-based tracking**: Can be bypassed by users with multiple IP addresses or using proxies
2. **Shared IPs**: Users behind NAT may share rate limits
3. **No authentication**: Current implementation doesn't require user authentication

### Future Enhancements

If rate limiting proves insufficient, consider:
1. Implementing user authentication and associating limits with user accounts
2. Adding browser fingerprinting for better user identification
3. Implementing CAPTCHA for repeated violations
4. Adding IP allowlisting for trusted networks
5. Implementing more sophisticated detection patterns (behavioral analysis)

## Monitoring

The RateLimiter logs configuration on startup and warnings when limits are exceeded. Monitor these logs to:
- Detect potential attacks
- Adjust rate limits based on legitimate usage patterns
- Identify problematic IPs

## Threat Model

This implementation addresses:

- **CAPEC-212 (Functionality Misuse)**: Prevents abuse of game/player creation
- **Resource exhaustion**: Prevents overwhelming the system
- **Service availability**: Ensures legitimate users can access the service

## Contact

For security issues or questions, please refer to the main repository [SECURITY.md](../../SECURITY.md).
6 changes: 6 additions & 0 deletions copi.owasp.org/fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ kill_signal = 'SIGTERM'
[env]
PHX_HOST = 'copi.fly.dev'
PORT = '8080'
RATE_LIMIT_GAME_CREATION_LIMIT = '20'
RATE_LIMIT_GAME_CREATION_WINDOW = '3600'
RATE_LIMIT_PLAYER_CREATION_LIMIT = '60'
RATE_LIMIT_PLAYER_CREATION_WINDOW = '3600'
RATE_LIMIT_CONNECTION_LIMIT = '333'
RATE_LIMIT_CONNECTION_WINDOW = '1'

[http_service]
internal_port = 8080
Expand Down
2 changes: 2 additions & 0 deletions copi.owasp.org/lib/copi/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ defmodule Copi.Application do
CopiWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Copi.PubSub},
# Start the RateLimiter for IP-based rate limiting (CAPEC-212 protection)
Copi.RateLimiter,
# Start the DNS clustering
{DNSCluster, query: Application.get_env(:copi, :dns_cluster_query) || :ignore},
# Start the Endpoint (http/https)
Comment on lines 15 to 20
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The PR title/description focuses on fixing the voting race condition, but this change also introduces a new global IP-based RateLimiter supervision child and related endpoint/socket behavior. Please either update the PR description/title to include the rate limiting feature (and its rationale/impact), or split rate limiting into a separate PR to keep scope and review risk manageable.

Copilot uses AI. Check for mistakes.
Expand Down
129 changes: 129 additions & 0 deletions copi.owasp.org/lib/copi/ip_helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
defmodule Copi.IPHelper do
@moduledoc """
Helper module for extracting IP addresses from Phoenix connections and sockets.

This module provides DRY (Don't Repeat Yourself) functionality for IP extraction
across different parts of the application (LiveView, controllers, channels).
"""

@doc """
Extracts the IP address from a Phoenix.Socket.

Returns the IP as a tuple (e.g., {127, 0, 0, 1}) or a fallback IP if not found.

## Examples
iex> get_ip_from_socket(socket)
{127, 0, 0, 1}
"""
def get_ip_from_socket(%Phoenix.LiveView.Socket{} = socket) do
case get_connect_info_ip(socket) do
nil -> {127, 0, 0, 1} # Fallback to localhost (for tests)
ip when is_tuple(ip) -> ip
ip -> ip
end
end

def get_ip_from_socket(%Phoenix.Socket{} = socket) do
case get_transport_ip(socket) do
nil -> {127, 0, 0, 1} # Fallback to localhost (for tests)
ip when is_tuple(ip) -> ip
ip -> ip
end
end

@doc """
Extracts the IP address from a Plug.Conn (used in controllers and plugs).

Supports X-Forwarded-For header when behind reverse proxies.
Returns the IP as a tuple (e.g., {127, 0, 0, 1}) or a fallback IP if not found.

## Examples
iex> get_ip_from_conn(conn)
{127, 0, 0, 1}
"""
def get_ip_from_conn(%Plug.Conn{} = conn) do
# Try to get real IP from X-Forwarded-For header first (for proxy scenarios)
case get_forwarded_ip(conn) do
nil ->
# Fall back to remote_ip
case conn.remote_ip do
nil -> {127, 0, 0, 1} # Fallback to localhost (for tests)
ip when is_tuple(ip) -> ip
ip -> ip
end
ip -> ip
end
end

@doc """
Converts an IP tuple to a string representation.

## Examples
iex> ip_to_string({127, 0, 0, 1})
"127.0.0.1"

iex> ip_to_string({0, 0, 0, 0, 0, 0, 0, 1})
"::1"
"""
def ip_to_string(ip) when is_tuple(ip) do
case :inet.ntoa(ip) do
{:error, _} -> inspect(ip)
ip_charlist -> to_string(ip_charlist)
end
end

def ip_to_string(ip) when is_binary(ip), do: ip
def ip_to_string(ip), do: inspect(ip)

# Private helpers

defp get_forwarded_ip(conn) do
# Get X-Forwarded-For header (rightmost IP is most recent proxy, leftmost is original client)
case Plug.Conn.get_req_header(conn, "x-forwarded-for") do
[] -> nil
[forwarded | _] ->
# Take the first (leftmost) IP from the comma-separated list
forwarded
|> String.split(",")
|> List.first()
|> String.trim()
|> parse_ip_string()
_ -> nil
end
end

defp parse_ip_string(ip_string) do
case :inet.parse_address(String.to_charlist(ip_string)) do
{:ok, ip_tuple} -> ip_tuple
{:error, _} -> nil
end
end

defp get_connect_info_ip(socket) do
# Access peer_data from connect_info using nil-safe get_in
# This prevents crashes when connect_info is nil (e.g., in tests)
case get_in(socket.private, [:connect_info, :peer_data]) do
%{address: address} -> address
_ -> nil
end
end
Comment on lines +102 to +109
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

get_connect_info_ip/1 can raise when socket.private[:connect_info] is missing (e.g., in tests or if connect_info isn’t configured), because socket.private[:connect_info][:peer_data] attempts to index into nil. Use a nil-safe access pattern (e.g., get_in(socket.private, [:connect_info, :peer_data])) or pattern match on %{connect_info: %{peer_data: ...}} so get_ip_from_socket/1 can reliably fall back instead of crashing.

Copilot uses AI. Check for mistakes.

defp get_transport_ip(socket) do
if Map.has_key?(socket, :transport_pid) && socket.transport_pid do
# Try to get from endpoint
case Process.info(socket.transport_pid, :dictionary) do
{:dictionary, dict} ->
Keyword.get(dict, :peer)
|> case do
{ip, _port} -> ip
_ -> nil
end

_ ->
nil
end
else
nil
end
end
end
Loading
Loading