Skip to content

Conversation

@immortal71
Copy link

Implemented separate IP-based rate limits for game creation, player creation, and WebSocket connections to protect against CAPEC-212 (Functionality Misuse) attacks and ensure service availability.

Changes:

  • Implemented Copi.RateLimiter GenServer for tracking rate limits per IP
  • Added separate rate limiting for:
    • Game creation: 10 per IP per hour
    • Player creation: 20 per IP per hour
    • Connections: 50 per IP per 5 minutes
  • Created shared IPHelper module for DRY IP extraction
  • Moved configuration to runtime.exs for proper env var handling
  • Removed 'unknown' IP fallback for security (raises error instead)
  • Adds comprehensive tests and SECURITY.md documentation

Addressed feedback from PR #1920:

  • Config moved to runtime.exs
  • Refactored duplicated get_connect_ip/1
  • Removed 'unknown' IP security issue

#1920

immortal71 and others added 30 commits December 2, 2025 20:25
Added Copi.RateLimiter (GenServer) and CopiWeb.Plugs.RateLimiter (Plug). Integrate rate limiting into LiveView for game creation and WebSocket connections. Added test and SECURITY.md. Making limits configurable via environment variables. Addresses OWASP#1877.
…ack)

-->Added :player_creation action to RateLimiter with separate limits
--> Default: 20 players per IP per hour (separate from game creation)
--> Applied rate limiting to PlayerLive.FormComponent
--> Added comprehensive tests for player creation limits
--> Updated documentation with implementation details

Fixes OWASP#1877
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@2028fbc...395ad32)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.49.0 to 2.49.1.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/[email protected]/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.49.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@1af3b93...8e8c483)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.45.3 to 5.45.4.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/[email protected]/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.45.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Added SQL queries to retrieve monthly game and player statistics.
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.45.5 to 5.45.6.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/[email protected]/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.45.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.12.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](psf/black@25.1.0...25.12.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.6 to 4.31.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@fe4161a...cf1bb45)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [pipenv](https://github.com/pypa/pipenv) from 2025.0.4 to 2025.1.1.
- [Release notes](https://github.com/pypa/pipenv/releases)
- [Changelog](https://github.com/pypa/pipenv/blob/main/CHANGELOG.md)
- [Commits](pypa/pipenv@v2025.0.4...v2025.1.1)

---
updated-dependencies:
- dependency-name: pipenv
  dependency-version: 2025.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.5 to 9.0.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](pytest-dev/pytest@8.3.5...9.0.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps the pip group with 1 update in the / directory: [urllib3](https://github.com/urllib3/urllib3).

Updates `urllib3` from 2.5.0 to 2.6.0
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](urllib3/urllib3@2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.0
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.5 to 9.0.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](pytest-dev/pytest@8.3.5...9.0.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.10.7 to 7.13.0.
- [Release notes](https://github.com/coveragepy/coveragepy/releases)
- [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst)
- [Commits](coveragepy/coveragepy@7.10.7...7.13.0)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps mvdan/shfmt from `20597e9` to `e414177`.

---
updated-dependencies:
- dependency-name: mvdan/shfmt
  dependency-version: e414177e424692cd21a5113216edeeeb56fc76b0ed2e5eb3a6c48404d5548a76
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.12.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](psf/black@25.1.0...25.12.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.4.0 to 4.5.1.
- [Release notes](https://github.com/tox-dev/platformdirs/releases)
- [Changelog](https://github.com/tox-dev/platformdirs/blob/main/CHANGES.rst)
- [Commits](tox-dev/platformdirs@4.4.0...4.5.1)

---
updated-dependencies:
- dependency-name: platformdirs
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.1.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](urllib3/urllib3@2.5.0...2.6.1)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps the pip group with 1 update in the / directory: [urllib3](https://github.com/urllib3/urllib3).

Updates `urllib3` from 2.5.0 to 2.6.0
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](urllib3/urllib3@2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.0
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps hexpm/elixir from 1.19-erlang-28.2-debian-bullseye-20251117 to 1.19-erlang-28.3-debian-bullseye-20251208.

---
updated-dependencies:
- dependency-name: hexpm/elixir
  dependency-version: 1.19-erlang-28.3-debian-bullseye-20251208
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.1 to 25.0.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [actions/cache](https://github.com/actions/cache) from 4.3.0 to 5.0.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](actions/cache@0057852...a783357)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [swoosh](https://github.com/swoosh/swoosh) from 1.19.8 to 1.19.9.
- [Release notes](https://github.com/swoosh/swoosh/releases)
- [Changelog](https://github.com/swoosh/swoosh/blob/main/CHANGELOG.md)
- [Commits](swoosh/swoosh@v1.19.8...v1.19.9)

---
updated-dependencies:
- dependency-name: swoosh
  dependency-version: 1.19.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.1.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](urllib3/urllib3@2.5.0...2.6.1)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.45.6 to 5.45.10.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/[email protected]/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.45.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.13.3 to 2.14.0.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](step-security/harden-runner@df199fb...20cf305)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [ecto_sql](https://github.com/elixir-ecto/ecto_sql) from 3.13.2 to 3.13.3.
- [Changelog](https://github.com/elixir-ecto/ecto_sql/blob/master/CHANGELOG.md)
- [Commits](elixir-ecto/ecto_sql@v3.13.2...v3.13.3)

---
updated-dependencies:
- dependency-name: ecto_sql
  dependency-version: 3.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
dependabot bot and others added 11 commits December 27, 2025 13:48
Bumps [phoenix](https://github.com/phoenixframework/phoenix) from 1.8.2 to 1.8.3.
- [Release notes](https://github.com/phoenixframework/phoenix/releases)
- [Changelog](https://github.com/phoenixframework/phoenix/blob/main/CHANGELOG.md)
- [Commits](phoenixframework/phoenix@v1.8.2...v1.8.3)

---
updated-dependencies:
- dependency-name: phoenix
  dependency-version: 1.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [phoenix_live_reload](https://github.com/phoenixframework/phoenix_live_reload) from 1.6.1 to 1.6.2.
- [Changelog](https://github.com/phoenixframework/phoenix_live_reload/blob/main/CHANGELOG.md)
- [Commits](phoenixframework/phoenix_live_reload@v1.6.1...v1.6.2)

---
updated-dependencies:
- dependency-name: phoenix_live_reload
  dependency-version: 1.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.49.1 to 2.49.2.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/[email protected]/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.49.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
@immortal71
Copy link
Author

@sydseter waiting for your review !!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements IP-based rate limiting to protect against CAPEC-212 (Functionality Misuse) attacks. However, there are several critical issues that prevent the feature from working correctly and compromise its security effectiveness.

Key Changes:

  • Added Copi.RateLimiter GenServer to track rate limits per IP address across three action types (game creation, player creation, connections)
  • Created CopiWeb.Helpers.IPHelper module to extract IP addresses from LiveView sockets
  • Integrated rate limiting checks into game creation, player creation, and WebSocket connection flows

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
copi.owasp.org/lib/copi/rate_limiter.ex Implements GenServer for tracking IP-based rate limits with configurable thresholds and time windows
copi.owasp.org/lib/copi_web/helpers/ip_helper.ex Extracts IP addresses from LiveView socket connections for rate limiting
copi.owasp.org/lib/copi/application.ex Adds RateLimiter to application supervision tree
copi.owasp.org/lib/copi_web/live/game_live/index.ex Adds connection rate limiting to mount function
copi.owasp.org/lib/copi_web/live/game_live/create_game_form.ex Adds game creation rate limiting with user-facing error messages
copi.owasp.org/lib/copi_web/live/player_live/form_component.ex Adds player creation rate limiting with user-facing error messages
copi.owasp.org/config/runtime.exs Configures rate limit thresholds via environment variables
copi.owasp.org/test/copi/rate_limiter_test.exs Comprehensive test suite for rate limiter functionality
copi.owasp.org/SECURITY.md Documents rate limiting configuration and security policies
copi.owasp.org/mix.lock Removes depth limitation from heroicons git dependency (unrelated change)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 14 to 20
case Copi.RateLimiter.check_rate(ip_address, :connection) do
{:ok, _remaining} ->
if connected?(socket) do
Phoenix.PubSub.subscribe(Copi.PubSub, "games")
end

{:ok, assign(socket, games: list_games())}
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

Connection rate limit is not enforced. The code checks the rate limit but never calls record_action for the connection action type, which means the connection count never increases and the limit can never be reached. After the check passes, you should call Copi.RateLimiter.record_action(ip_address, :connection) to actually track the connection attempt.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why aren't you calling record_action?

Comment on lines 147 to 159
test "allows requests after window expires" do
ip = "192.168.100.1"

# This test would require waiting for the window to expire
# In a real scenario, you might want to use a mock timer or
# make the window configurable for testing

assert {:ok, _remaining} = RateLimiter.check_rate(ip, :game_creation)
RateLimiter.record_action(ip, :game_creation)

# Verify request was recorded
assert {:ok, _remaining} = RateLimiter.check_rate(ip, :game_creation)
end
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

Test doesn't validate its stated purpose. This test claims to verify that "requests are allowed after window expires" but it doesn't actually wait for any time window to expire. The test only verifies that two consecutive requests are allowed, which is already covered by other tests. Either remove this test or implement proper window expiration testing using techniques mentioned in the comment (e.g., allow injecting a clock/time function for testing).

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please implement what the comment says.

Comment on lines 115 to 120
case Copi.RateLimiter.check_rate(ip_address, :game_creation) do
{:ok, _remaining} ->
case Cornucopia.create_game(game_params) do
{:ok, game} ->
# Record the action after successful creation
Copi.RateLimiter.record_action(ip_address, :game_creation)
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

Race condition vulnerability: The separation of check_rate and record_action creates a TOCTOU (Time-of-Check-Time-of-Use) issue. Multiple concurrent requests from the same IP could all pass the check_rate call before any of them calls record_action, effectively bypassing the rate limit. For example, if the limit is 10 and there are currently 9 recorded actions, 5 concurrent requests could all check (seeing 9 < 10), all pass, and all record, resulting in 14 total actions. Consider making check_rate atomically record the action when it returns {:ok, _}, or create a check_and_record/2 function that does both operations in a single GenServer call.

Copilot uses AI. Check for mistakes.
@immortal71 immortal71 force-pushed the feat/rate-limiting-clean branch from 4749354 to 648cfe9 Compare January 2, 2026 09:02
@immortal71
Copy link
Author

@sydseter waiting for review

Comment on lines +1 to +40
defmodule CopiWeb.Helpers.IPHelper do
@moduledoc """
Helper functions for extracting and formatting IP addresses from socket connections.
"""

@doc """
Extracts the IP address from a LiveView socket connection.

Returns a string representation of the IP address (IPv4 or IPv6).
Raises an error if the IP address cannot be determined, as this should never
happen in a properly configured backend environment.

## Examples

iex> get_connect_ip(socket)
"192.168.1.1"

iex> get_connect_ip(socket)
"2001:db8::1"
"""
def get_connect_ip(socket) do
case Phoenix.LiveView.get_connect_info(socket, :peer_data) do
%{address: {a, b, c, d}} ->
# IPv4 address
"#{a}.#{b}.#{c}.#{d}"

%{address: {a, b, c, d, e, f, g, h}} ->
# IPv6 address - format as colon-separated hex
[a, b, c, d, e, f, g, h]
|> Enum.map(&Integer.to_string(&1, 16))
|> Enum.join(":")

nil ->
raise "Unable to determine IP address from socket connection. peer_data is nil."

other ->
raise "Unexpected peer_data format: #{inspect(other)}"
end
end
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

We definetly need test coverage on this.

@@ -0,0 +1,183 @@
defmodule Copi.RateLimiterTest do
Copy link
Collaborator

Choose a reason for hiding this comment

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

The test is failing.

Tests are failing. Please ensure that they pass:

..09:15:37.703 [error] GenServer Copi.RateLimiter terminating
** (ArgumentError) could not put/update key :connection on a nil value
(elixir 1.19.2) lib/access.ex:436: Access.get_and_update/3
(elixir 1.19.2) lib/map.ex:965: Map.get_and_update/3
(elixir 1.19.2) lib/kernel.ex:2950: Kernel.put_in/3
(copi 0.1.0) lib/copi/rate_limiter.ex:144: Copi.RateLimiter.handle_call/3
(stdlib 7.1) gen_server.erl:2470: :gen_server.try_handle_call/4
(stdlib 7.1) gen_server.erl:2499: :gen_server.handle_msg/3
(stdlib 7.1) proc_lib.erl:333: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.8256.0>): {:check_and_record, "127.0.0.1", :connection}

  1. test Index saves new game (CopiWeb.GameLiveTest)
    test/copi_web/live/game_live_test.exs:31
    ** (exit) exited in: GenServer.call(Copi.RateLimiter, {:check_and_record, "127.0.0.1", :connection}, 5000)
    ** (EXIT) an exception was raised:
    ** (ArgumentError) could not put/update key :connection on a nil value
    (elixir 1.19.2) lib/access.ex:436: Access.get_and_update/3
    (elixir 1.19.2) lib/map.ex:965: Map.get_and_update/3
    (elixir 1.19.2) lib/kernel.ex:2950: Kernel.put_in/3
    (copi 0.1.0) lib/copi/rate_limiter.ex:144: Copi.RateLimiter.handle_call/3
    (stdlib 7.1) gen_server.erl:2470: :gen_server.try_handle_call/4
    (stdlib 7.1) gen_server.erl:2499: :gen_server.handle_msg/3
    (stdlib 7.1) proc_lib.erl:333: :proc_lib.init_p_do_apply/3
    code: {:ok, index_live, _html} = live(conn, "/games")
    stacktrace:
    (elixir 1.19.2) lib/gen_server.ex:1142: GenServer.call/3
    (copi 0.1.0) lib/copi_web/live/game_live/index.ex:14: CopiWeb.GameLive.Index.mount/3
    (phoenix_live_view 1.0.17) lib/phoenix_live_view/utils.ex:348: anonymous fn/6 in Phoenix.LiveView.Utils.maybe_call_live_view_mount!/5
    (telemetry 1.3.0) c:/Users/JSydseter/src/cornucopia-test/copi.owasp.org/deps/telemetry/src/telemetry.erl:324: :telemetry.span/3
    (phoenix_live_view 1.0.17) lib/phoenix_live_view/static.ex:321: Phoenix.LiveView.Static.call_mount_and_handle_params!/5
    (phoenix_live_view 1.0.17) lib/phoenix_live_view/static.ex:155: Phoenix.LiveView.Static.do_render/4
    (phoenix_live_view 1.0.17) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
    (phoenix 1.8.3) lib/phoenix/router.ex:416: Phoenix.Router.call/5
    (copi 0.1.0) lib/copi_web/endpoint.ex:1: CopiWeb.Endpoint.plug_builder_call/2
    (copi 0.1.0) lib/copi_web/endpoint.ex:1: CopiWeb.Endpoint.call/2
    (phoenix 1.8.3) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5
    test/copi_web/live/game_live_test.exs:32: (test)

.09:15:37.808 [error] GenServer #PID<0.8278.0> terminating
** (RuntimeError) attempted to read connect_info outside of CopiWeb.PlayerLive.Index.mount/3.

connect_info only exists while mounting. If you require access to this information
after mount, store the state in socket assigns.

(phoenix_live_view 1.0.17) lib/phoenix_live_view.ex:1338: Phoenix.LiveView.raise_root_and_mount_only!/2
(copi 0.1.0) lib/copi_web/helpers/ip_helper.ex:22: CopiWeb.Helpers.IPHelper.get_connect_ip/1
(copi 0.1.0) lib/copi_web/live/player_live/form_component.ex:79: CopiWeb.PlayerLive.FormComponent.save_player/3
(phoenix_live_view 1.0.17) lib/phoenix_live_view/channel.ex:742: anonymous fn/4 in Phoenix.LiveView.Channel.inner_component_handle_event/4
(telemetry 1.3.0) c:/Users/JSydseter/src/cornucopia-test/copi.owasp.org/deps/telemetry/src/telemetry.erl:324: :telemetry.span/3
(phoenix_live_view 1.0.17) lib/phoenix_live_view/diff.ex:209: Phoenix.LiveView.Diff.write_component/4
(phoenix_live_view 1.0.17) lib/phoenix_live_view/channel.ex:663: Phoenix.LiveView.Channel.component_handle/4
(stdlib 7.1) gen_server.erl:2434: :gen_server.try_handle_info/3
(stdlib 7.1) gen_server.erl:2420: :gen_server.handle_msg/3
(stdlib 7.1) proc_lib.erl:333: :proc_lib.init_p_do_apply/3

Last message: %Phoenix.Socket.Message{topic: "lv:phx-GIizbcVqAYCuoFwC", event: "event", payload: %{"cid" => 1, "event" => "save", "type" => "form", "value" => "player[game_id]=01KEEASVRXNZQE49TEV4V3W80F&player[name]=some+updated+name"}, ref: "2", join_ref: 0}
09:15:37.811 [error] GenServer #PID<0.8274.0> terminating
** (RuntimeError) attempted to read connect_info outside of CopiWeb.PlayerLive.Index.mount/3.

connect_info only exists while mounting. If you require access to this information
after mount, store the state in socket assigns.

(phoenix_live_view 1.0.17) lib/phoenix_live_view.ex:1338: Phoenix.LiveView.raise_root_and_mount_only!/2
(copi 0.1.0) lib/copi_web/helpers/ip_helper.ex:22: CopiWeb.Helpers.IPHelper.get_connect_ip/1
(copi 0.1.0) lib/copi_web/live/player_live/form_component.ex:79: CopiWeb.PlayerLive.FormComponent.save_player/3
(phoenix_live_view 1.0.17) lib/phoenix_live_view/channel.ex:742: anonymous fn/4 in Phoenix.LiveView.Channel.inner_component_handle_event/4
(telemetry 1.3.0) c:/Users/JSydseter/src/cornucopia-test/copi.owasp.org/deps/telemetry/src/telemetry.erl:324: :telemetry.span/3
(phoenix_live_view 1.0.17) lib/phoenix_live_view/diff.ex:209: Phoenix.LiveView.Diff.write_component/4
(phoenix_live_view 1.0.17) lib/phoenix_live_view/channel.ex:663: Phoenix.LiveView.Channel.component_handle/4
(stdlib 7.1) gen_server.erl:2434: :gen_server.try_handle_info/3
(stdlib 7.1) gen_server.erl:2420: :gen_server.handle_msg/3
(stdlib 7.1) proc_lib.erl:333: :proc_lib.init_p_do_apply/3

Last message: {:EXIT, #PID<0.8272.0>, {%RuntimeError{message: "attempted to read connect_info outside of CopiWeb.PlayerLive.Index.mount/3.\n\nconnect_info only exists while mounting. If you require access to this information\nafter mount, store the state in socket assigns.\n"}, [{Phoenix.LiveView, :raise_root_and_mount_only!, 2, [file: ~c"lib/phoenix_live_view.ex", line: 1338]}, {CopiWeb.Helpers.IPHelper, :get_connect_ip, 1, [file: ~c"lib/copi_web/helpers/ip_helper.ex", line: 22]}, {CopiWeb.PlayerLive.FormComponent, :save_player, 3, [file: ~c"lib/copi_web/live/player_live/form_component.ex", line: 79]}, {Phoenix.LiveView.Channel, :"-inner_component_handle_event/4-fun-0-", 4, [file: ~c"lib/phoenix_live_view/channel.ex", line: 742]}, {:telemetry, :span, 3, [file: ~c"c:/Users/JSydseter/src/cornucopia-test/copi.owasp.org/deps/telemetry/src/telemetry.erl", line: 324]}, {Phoenix.LiveView.Diff, :write_component, 4, [file: ~c"lib/phoenix_live_view/diff.ex", line: 209]}, {Phoenix.LiveView.Channel, :component_handle, 4, [file: ~c"lib/phoenix_live_view/channel.ex", line: 663]}, {:gen_server, :try_handle_info, 3, [file: ~c"gen_server.erl", line: 2434]}, {:gen_server, :handle_msg, 3, [file: ~c"gen_server.erl", line: 2420]}, {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 333]}]}}

  1. test Index saves new player (CopiWeb.PlayerLiveTest)
    test/copi_web/live/player_live_test.exs:33
    ** (EXIT from #PID<0.8272.0>) an exception was raised:
    ** (RuntimeError) attempted to read connect_info outside of CopiWeb.PlayerLive.Index.mount/3.

    connect_info only exists while mounting. If you require access to this information
    after mount, store the state in socket assigns.

        (phoenix_live_view 1.0.17) lib/phoenix_live_view.ex:1338: Phoenix.LiveView.raise_root_and_mount_only!/2
        (copi 0.1.0) lib/copi_web/helpers/ip_helper.ex:22: CopiWeb.Helpers.IPHelper.get_connect_ip/1
        (copi 0.1.0) lib/copi_web/live/player_live/form_component.ex:79: CopiWeb.PlayerLive.FormComponent.save_player/3
        (phoenix_live_view 1.0.17) lib/phoenix_live_view/channel.ex:742: anonymous fn/4 in Phoenix.LiveView.Channel.inner_component_handle_event/4
        (telemetry 1.3.0) c:/Users/JSydseter/src/cornucopia-test/copi.owasp.org/deps/telemetry/src/telemetry.erl:324: :telemetry.span/3
        (phoenix_live_view 1.0.17) lib/phoenix_live_view/diff.ex:209: Phoenix.LiveView.Diff.write_component/4
        (phoenix_live_view 1.0.17) lib/phoenix_live_view/channel.ex:663: Phoenix.LiveView.Channel.component_handle/4
        (stdlib 7.1) gen_server.erl:2434: :gen_server.try_handle_info/3
        (stdlib 7.1) gen_server.erl:2420: :gen_server.handle_msg/3
        (stdlib 7.1) proc_lib.erl:333: :proc_lib.init_p_do_apply/3
    
  2. test player creation rate limiting blocks player creation over the limit (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:111
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

  3. test connection rate limiting blocks connections over the limit (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:82
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

  4. test player creation rate limiting allows player creation under the limit (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:100
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

  5. test connection rate limiting allows connections under the limit (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:71
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

  6. test IP clearing clears rate limit data for an IP (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:167
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

  7. test player creation rate limiting player creation limit is separate from game creation limit (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:127
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

  8. test configuration returns current configuration (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:147
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

  9. test game creation rate limiting different IPs have independent limits (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:50
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

  10. test game creation rate limiting allows requests under the limit (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:21
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

  11. test game creation rate limiting blocks requests over the limit (Copi.RateLimiterTest)
    test/copi/rate_limiter_test.exs:34
    ** (MatchError) no match of right hand side value:

    {:error, {:already_started, #PID<0.8258.0>}}
    

    stacktrace:
    test/copi/rate_limiter_test.exs:8: Copi.RateLimiterTest.__ex_unit_setup_0/1
    test/copi/rate_limiter_test.exs:1: Copi.RateLimiterTest.ex_unit/2

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am still getting the same test errors.

Copy link
Collaborator

@sydseter sydseter left a comment

Choose a reason for hiding this comment

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

Please have a look at my comments. Thank you for your effort.

@sydseter sydseter requested a review from Copilot January 8, 2026 08:27
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 16 to 20
if connected?(socket) do
Phoenix.PubSub.subscribe(Copi.PubSub, "games")
end

{:ok, assign(socket, games: list_games())}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

When rate limiting succeeds, list_games() is called but the function is not visible in this diff. The original code assigned nil to games. If list_games/0 doesn't exist in this module, this will cause a compilation error.

Copilot uses AI. Check for mistakes.
Comment on lines 84 to 86
case Cornucopia.create_player(player_params) do
{:ok, player} ->
{:ok, updated_game} = Cornucopia.Game.find(socket.assigns.player.game_id)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The nesting creates complex control flow with two levels of case statements. Consider extracting the player creation logic into a separate private function (e.g., do_create_player/2) to improve readability and reduce nesting depth.

Copilot uses AI. Check for mistakes.
if count < config.max_requests do
# Atomically record the action before returning success
updated_requests = [now | valid_requests]
new_requests = put_in(state.requests, [ip_address, action], updated_requests)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

Using put_in/3 with a list of keys on a potentially non-existent nested path will raise an error if the IP address key doesn't exist in the map. Use Kernel.put_in/3 or ensure the path exists first, or consider using Map.put/3 with Map.get/3 for safer nested map updates.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be an issue and should be dealt with.

Comment on lines 173 to 177
new_requests = put_in(
state.requests,
[ip_address, action],
updated_requests
)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

Similar to the issue in check_and_record, using put_in/3 with a path that may not exist will raise an error. This occurs when recording an action for a new IP address that hasn't been seen before.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here.

@sydseter
Copy link
Collaborator

The tests are still throwing errors which indicate that there is something wrong with how the rate limiter is implemented.

@sydseter
Copy link
Collaborator

Please update your branch a before continuing.

… store IP in assigns, fix tests

- Added atomic check_and_record() function to prevent race conditions (TOCTOU)
- Created IPHelper module for DRY IP extraction with proper error handling
- Store IP address in socket assigns during mount to avoid connect_info access errors
- Used Map.update() instead of put_in() to safely handle new IPs
- Updated all rate limiting code to use check_and_record atomically
- Fixed tests: change to async: false, use unique IPs, use check_and_record
- Adedd comprehensive IP helper tests
- Removde duplicate get_connect_ip() functions
@immortal71 immortal71 force-pushed the feat/rate-limiting-clean branch from 01d7577 to 4918309 Compare January 31, 2026 13:13
@immortal71
Copy link
Author

@sydseter Can you check, i have updated as you have suggested !!

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.

3 participants