Skip to content

fix: add rate limiting for API authentication and token validation - #2366

Merged
elphizu merged 8 commits into
developfrom
fix/api-token-rate-limiter
Aug 27, 2026
Merged

fix: add rate limiting for API authentication and token validation#2366
elphizu merged 8 commits into
developfrom
fix/api-token-rate-limiter

Conversation

@elphizu

@elphizu elphizu commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds rate limiting to rtMedia API authentication and token validation endpoints to reduce brute-force and token-guessing attempts.

Changes

  • Adds RTMediaApiRateLimiter for tracking failed authentication attempts.
  • Applies separate limits for:
    • Login failures per client IP: 20 attempts per 5 minutes.
    • Login failures per username/email identifier: 5 attempts per 5 minutes.
    • Invalid API-token submissions per client IP: 20 attempts per 5 minutes.
  • Uses a default five-minute rate-limit window.
  • Defines named constants for the default rate-limit window and attempt limits.
  • Returns HTTP 429 Too Many Requests with a Retry-After header when a limit is reached.
  • Uses API status code 200006 for rate-limited login attempts.
  • Replaces username-specific and password-specific login errors with a generic invalid username or password response to prevent username enumeration.
  • Clears the identifier counter after successful authentication while retaining the IP counter.
  • Hashes IP addresses and login identifiers before using them in cache keys.
  • Provides filters for customizing the limits, window, and resolved client IP:
rtmedia_api_rate_limit_window
rtmedia_api_login_ip_limit
rtmedia_api_login_identifier_limit
rtmedia_api_token_attempt_limit
rtmedia_api_client_ip

Implementation details

When a persistent object cache such as Redis or Memcached is active, counters use wp_cache_add() and wp_cache_incr().

When no persistent object cache is configured, counters fall back to WordPress transients.

By default, only REMOTE_ADDR is used to identify the client. Forwarded headers are not trusted automatically because they may be spoofed unless a trusted reverse proxy sanitizes them.

Limitations

Counter atomicity

Rate-limit increments are atomic only when the configured persistent object-cache backend provides atomic add and increment operations.

The transient fallback is not atomic. Concurrent requests can read the same counter value and overwrite one another, causing some attempts to go uncounted. Rate limiting therefore remains best-effort on installations without a persistent object cache.

There is also a small expiration race with persistent caches: if a key expires between wp_cache_add() and wp_cache_incr(), the fallback wp_cache_set() may overlap with another request. This does not disable rate limiting, but exact counting is not guaranteed at the window boundary.

REMOTE_ADDR and reverse proxies

REMOTE_ADDR may represent a load balancer, CDN, or reverse proxy rather than the original client. On these installations, multiple users can share one rate-limit bucket and potentially rate-limit one another.

The rtmedia_api_client_ip filter allows an installation to resolve the client IP from infrastructure-specific headers. The callback must only accept headers that are stripped and rewritten by a trusted proxy.

IP-format validation alone does not make a forwarded header trustworthy - a client can still supply a syntactically valid but forged IP address if the proxy does not sanitize that header.

@elphizu
elphizu marked this pull request as ready for review August 26, 2026 12:00
Copilot AI balanced review requested due to automatic review settings August 26, 2026 12:00

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread app/main/controllers/api/RTMediaApiRateLimiter.php
Copilot AI review requested due to automatic review settings August 26, 2026 14:40

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

app/main/controllers/api/RTMediaJsonApi.php:342

  • Retry-After always advertises a fresh full window, but this is a fixed-window counter whose expiry starts at the first failure. If the limit is reached near the end of that window, the login can be retried much sooner than this header says, so compliant clients may wait almost five unnecessary minutes. Return the remaining lifetime of whichever blocking bucket(s) are active (the maximum when both IP and identifier buckets block) rather than get_window().
				header( 'Retry-After: ' . $rate_limiter->get_window() );

app/main/controllers/api/RTMediaJsonApiFunctions.php:179

  • This also sends the configured full window instead of the token bucket's remaining lifetime. For a fixed window that is nearly expired, clients are told to wait up to five minutes even though validation will be accepted seconds later. Expose the token bucket's remaining TTL from the rate limiter and use that value for Retry-After.
			header( 'Retry-After: ' . $rate_limiter->get_window() );

Copilot AI review requested due to automatic review settings August 27, 2026 06:09

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@elphizu

elphizu commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

good catch by copilot

fixed by returning the remaining TTL for the blocking IP or username/email bucket.

when both buckets are blocking Retry-After uses the longer TTL so both limits have expired before the client retries

stack merge was automatically disabled August 27, 2026 09:44

Pull Request is not mergeable

Base automatically changed from fix/api-token-security to develop August 27, 2026 09:48
@elphizu
elphizu force-pushed the fix/api-token-rate-limiter branch from 9432de9 to 1f8b110 Compare August 27, 2026 09:48
@elphizu elphizu changed the title Add rate limiting for API authentication and token validation fix: add rate limiting for API authentication and token validation Aug 27, 2026
@rtBot

rtBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Unable to PHPCS or SVG scan one or more files due to error running PHPCS/SVG scanner:

  • app/main/controllers/api/RTMediaApiRateLimiter.php
  • app/main/controllers/api/RTMediaJsonApi.php
  • app/main/controllers/api/RTMediaJsonApiFunctions.php

The error may be temporary. If the error persists, please contact a human (commit-ID: d42bc33).

@elphizu
elphizu merged commit ed4a2d7 into develop Aug 27, 2026
6 checks passed
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.

4 participants