fix: security hardening - rate limiting and brute-force protection for bearer auth - #31
Conversation
…l substring matching - Add pluggable Analyzer interface with PlainAnalyzer (default) and DomainAnalyzer - PlainAnalyzer uses prefix-only token matching to prevent false positives (e.g., 'api' matching 'rapid', 'cat' matching 'category') - _QUERY_EXPANSIONS moved behind DomainAnalyzer, not used by default - Add AnalyzerKind config (DMA_ANALYZER_KIND env var) to opt-in to domain expansions - Update _to_fts_query to use >= 3 char prefix matching (was > 3) - Add adversarial test cases for prefix-only matching (cat/category, api/rapid, art/particle) - Use DomainAnalyzer in benchmark runner for corpus parity Fixes #27
…r bearer auth - Add InMemoryRateLimiter with sliding-window + lockout per source (IP) - Configure via DMA_AUTH_MAX_ATTEMPTS (default 5), DMA_AUTH_WINDOW_SECONDS (default 60), DMA_AUTH_LOCKOUT_SECONDS (default 300) - Extract client IP from X-Forwarded-For header - Log failed auth attempts with source IP (no key logged) - Successful auth resets failure counter for that source - Locked-out sources get 429 with retry message - Add AuthLimits dataclass to config - Add 8 comprehensive tests for rate limiting behavior Fixes #26
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Note
This report is out of date. Scroll down for Devin Review's latest report on this PR.
Devin Review found 6 potential issues.
3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| def _client_source(x_forwarded_for: str | None) -> str: | ||
| if x_forwarded_for: | ||
| return x_forwarded_for.split(",")[0].strip() | ||
| return "unknown" |
There was a problem hiding this comment.
- Trust X-Forwarded-For only when DMA_TRUST_FORWARDED_FOR is set; fall back to the transport peer instead of a shared unknown bucket - Evict stale rate-limiter sources and cap the tracked map size - Validate auth limit env values as positive integers - Use analyzer expansions when building the FTS candidate query and credit expansion matches to their query token - Normalise naive datetimes to UTC in _utc_isoformat - Repair the offset-expiry recall test and refresh the benchmark failure baseline Co-Authored-By: krishna <87197325+krishna3554@users.noreply.github.com>
The 'cloud' builder is not configured on GitHub-hosted runners, so the container validation step failed with 'no builder "cloud" found'. Co-Authored-By: krishna <87197325+krishna3554@users.noreply.github.com>
uv pip install has no --locked flag, so the image build failed; export the lock to a requirements file and install from it. Co-Authored-By: krishna <87197325+krishna3554@users.noreply.github.com>
There was a problem hiding this comment.
Devin Review found 3 new potential issues.
3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| return self._analyzer.expand_tokens({ | ||
| token | ||
| for token in (cls._normalise_token(raw_token) for raw_token in re.findall(r"[\w]+", text, flags=re.UNICODE)) | ||
| for token in (self._normalise_token(raw_token) for raw_token in re.findall(r"[\w]+", text, flags=re.UNICODE)) | ||
| if token and token not in _STOPWORDS and len(token) > 2 | ||
| }) |
There was a problem hiding this comment.
🟡 Synonyms weaken multi-word matching
For multi-word domain queries, _important_tokens counts an original word and its synonym separately. One synonym can pass the threshold while other requested words are absent.
| return self._analyzer.expand_tokens({ | |
| token | |
| for token in (cls._normalise_token(raw_token) for raw_token in re.findall(r"[\w]+", text, flags=re.UNICODE)) | |
| for token in (self._normalise_token(raw_token) for raw_token in re.findall(r"[\w]+", text, flags=re.UNICODE)) | |
| if token and token not in _STOPWORDS and len(token) > 2 | |
| }) | |
| return { | |
| token | |
| for token in (self._normalise_token(raw_token) for raw_token in re.findall(r"[\w]+", text, flags=re.UNICODE)) | |
| if token and token not in _STOPWORDS and len(token) > 2 | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
| overflow = len(self._sources) - self._max_sources + 1 | ||
| if overflow <= 0: | ||
| return | ||
| stalest = sorted(self._sources, key=self._last_activity)[:overflow] | ||
| for key in stalest: | ||
| del self._sources[key] |
| repository = SQLiteMemoryRepository(runtime_settings.database_path) | ||
| analyzer = get_analyzer(runtime_settings.analyzer_kind) | ||
| repository = SQLiteMemoryRepository(runtime_settings.database_path, analyzer=analyzer) | ||
| rate_limiter = InMemoryRateLimiter(runtime_settings.auth_limits) |
There was a problem hiding this comment.
Summary
Fixes issue #26 by adding rate limiting and brute-force protection for the bearer token authentication.
Changes
InMemoryRateLimiter - Per-source (IP) sliding-window rate limiter with exponential backoff/lockout:
Audit Logging - Failed auth attempts logged with source IP (no keys logged):
AuthLimits Configuration - New dataclass added to Settings with env var support
Tests - 8 new tests covering:
Testing
All 35 tests pass:
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-7.4.4, pluggy-1.4.0 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/krishna-lokhande
collecting ... collected 0 items
============================ no tests ran in 0.00s =============================