Add blocklist domain allowlist for SSRF protection#990
Conversation
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Visual recap — skippedThe visual recap job did not run for this pull request. This is informational only and does not block the PR. Recap skipped for |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
| if not hostname: | ||
| return False | ||
|
|
||
| domains_to_check = ( | ||
| allowed_domains | ||
| if allowed_domains is not None | ||
| else _ALLOWED_BLOCKLIST_DOMAINS | ||
| ) |
There was a problem hiding this comment.
📝 Info: LRU cache correctness depends on cache_clear() being called on every allowlist mutation
The validate_folder_url function at main.py:1218 is decorated with @lru_cache(maxsize=128) and reads from the global _ALLOWED_BLOCKLIST_DOMAINS when allowed_domains is None (which is every call site). This means the cache key (url, None) can produce stale results if the global changes without clearing the cache. Currently this is safe because set_allowed_blocklist_domains at main.py:1276 calls validate_folder_url.cache_clear(), and sync_profile at main.py:2649 also clears it. However, any future code that directly assigns to _ALLOWED_BLOCKLIST_DOMAINS without going through set_allowed_blocklist_domains would introduce a cache staleness bug. Consider adding a comment on the global variable declaration (main.py:531) warning that it must only be mutated via the setter function.
Was this helpful? React with 👍 or 👎 to provide feedback.
| @pytest.fixture(autouse=True) | ||
| def _default_test_blocklist_allowlist(): | ||
| main.set_allowed_blocklist_domains( | ||
| [ | ||
| "raw.githubusercontent.com", | ||
| "github.com", | ||
| "example.com", | ||
| ] | ||
| ) | ||
| yield | ||
| main.set_allowed_blocklist_domains(None) |
There was a problem hiding this comment.
📝 Info: Autouse fixture adds example.com to allowed domains for all tests
The new autouse fixture in conftest.py:13-23 adds example.com to the allowed blocklist domains for every test. This is necessary because many existing SSRF tests (e.g., test_ssrf.py, test_ssrf_enhanced.py, test_ssrf_loopback.py, test_ssrf_link_local.py) use URLs like https://internal.example.com/... or https://public.example.com/.... Without example.com in the allowlist, these tests would fail at the new domain check before reaching the IP validation they're actually testing. This is a correct fix, but it means test authors must be aware that example.com is always allowed — tests that intend to verify domain rejection must use a domain not in this list (as test_ssrf_protection.py:27 does with evil.com).
Was this helpful? React with 👍 or 👎 to provide feedback.
| def _is_allowed_blocklist_domain( | ||
| hostname: str, allowed_domains: frozenset[str] | ||
| ) -> bool: | ||
| return any( | ||
| hostname == domain or hostname.endswith(f".{domain}") | ||
| for domain in allowed_domains | ||
| ) |
There was a problem hiding this comment.
📝 Info: Subdomain matching in _is_allowed_blocklist_domain could match unintended domains
The _is_allowed_blocklist_domain function at main.py:1209-1215 checks hostname.endswith(f".{domain}"). If someone adds a short domain like com to the allowlist, it would match any .com domain. This is by design (the allowlist is operator-controlled), but worth noting that the validation in _validate_allowed_blocklist_domains at main.py:1074-1083 only checks that entries are non-empty strings — it doesn't validate that they look like reasonable domain names (e.g., containing at least one dot). An operator typo like com instead of example.com would silently weaken the SSRF protection.
Was this helpful? React with 👍 or 👎 to provide feedback.
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
Co-Authored-By: Abhi 🐝 Mehrotra <abhimehro@pm.me>
|
/cs-agent skill:fix-code-health-degradations |
|
There was a problem hiding this comment.
Automated PR Review — ESCALATE (2026-07-14)
Disposition: ESCALATE — security-sensitive SSRF allowlist change requires human approval.
Gates:
- CI functional checks: green (tests, bandit, mypy, benchmark)
- CodeScene: FAIL (code health regression on
main.py) - Security gate: trust-boundary change — outbound fetch domain allowlist affects production SSRF posture
Required before merge:
- Human security review of allowlist completeness and bypass vectors
- CodeScene remediation —
/cs-agent skill:fix-code-health-degradationsposted for follow-up - Confirm benchmark delta vs pre-SSRF
mainbaseline is acceptable
Phase 2 salvage agent may draft infra fixes; this automation will not squash-merge autonomously.
Sent by Cursor Automation: Automated PR Review & Consolidation Agent
|
/cs-agent skill:fix-code-health-degradations |
|
|
/cs-agent skill:fix-code-health-degradations |
1 similar comment
|
/cs-agent skill:fix-code-health-degradations |
|
- Extract batch processing logic into _process_batches_with_executor helper - Reduce cyclomatic complexity from 14 to acceptable levels - Eliminate deep nested complexity (depth 4 -> 2) - Fix Brain Method and Complex Method code smells - Reduce function arguments by grouping related parameters - Quality gates now pass with no degradations
| ctx.existing_rules.update(result) | ||
|
|
||
| render_progress_bar(successful_batches, total_batches, progress_label) | ||
| with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: |
There was a problem hiding this comment.
📝 Info: Fallback thread pool worker count silently increased from 3 to 4
The fallback ThreadPoolExecutor (used when no shared executor is supplied) was changed from max_workers=3 to max_workers=4 (main.py:2377). The old value of 3 matched DELETE_WORKERS (main.py:483) which is documented as "Conservative for DELETE operations due to rate limits." The increase is not mentioned in the CHANGELOG or PR description. While push operations may tolerate higher concurrency than deletes, this could increase the likelihood of hitting API rate limits for users who rely on the fallback path.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Intentional; the no-executor path keeps the current push concurrency. Happy to dial it back if you want a stricter cap.
| def set_allowed_blocklist_domains(domains: list[str] | None) -> None: | ||
| """Set the runtime allowed blocklist domains for SSRF protection.""" | ||
| global _ALLOWED_BLOCKLIST_DOMAINS | ||
| if domains and len(domains) > 0: | ||
| _ALLOWED_BLOCKLIST_DOMAINS = frozenset(domain.lower() for domain in domains) | ||
| else: | ||
| _ALLOWED_BLOCKLIST_DOMAINS = DEFAULT_ALLOWED_BLOCKLIST_DOMAINS | ||
| # validate_folder_url() is cached, so any allowlist change must clear it. | ||
| validate_folder_url.cache_clear() |
There was a problem hiding this comment.
📝 Info: Empty allowed_blocklist_domains list silently falls back to defaults
If a user sets allowed_blocklist_domains: [] in their config, _validate_allowed_blocklist_domains (main.py:1083-1092) accepts it as valid. However, set_allowed_blocklist_domains([]) (main.py:1277-1285) treats the empty list as falsy and falls back to DEFAULT_ALLOWED_BLOCKLIST_DOMAINS. This creates a disconnect: validation says the config is valid, but the runtime silently ignores it. A user who explicitly sets an empty list might expect it to have an effect (either disabling the allowlist or blocking all domains). The current behavior is safe (defaults are applied), but could be confusing. Consider either rejecting empty lists in validation or documenting this fallback behavior.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Intentional safety fallback: an empty list falls back to the default allowlist so we don't silently disable SSRF protection. I can document that explicitly if you'd like.
There was a problem hiding this comment.
Automated PR Salvage — ESCALATE (2026-07-14 evening)
Disposition: ESCALATE — unchanged; blocker analysis refreshed.
CI update since Phase 1
| Check | Status |
|---|---|
| CodeScene | ✅ PASS (cs-agent remediation complete) |
| test / mypy / bandit / CodeQL | ✅ PASS |
| ruff | ❌ FAIL — 1 fixable blank-line whitespace in main.py (~line 2356) |
| benchmark | ❌ FAIL — push_rules benchmarks 1.80× / 1.64× slower (SSRF validation overhead) |
Maintainer actions
- Accept benchmark trade-off for SSRF allowlist or optimize validation path.
- Apply trivial
ruff --fixwhitespace correction on branch. - Human security review of allowlist semantics before merge.
No salvage draft opened — branch is MERGEABLE with no conflicts (Lesson 0df). Policy S1: no autonomous merge.
Sent by Cursor Automation: Automated PR Salvage & Consolidation Agent
There was a problem hiding this comment.
Automated PR review (2026-07-15 cron)
Disposition: ESCALATE — unchanged from prior sessions.
Blockers:
- Required checks still failing:
ruff,benchmark - SSRF allowlist touches external fetch trust boundary (ABHI-1347)
Next step: Human security review of allowlist semantics and benchmark regression vs pre-SSRF baseline. Do not auto-merge until benchmark is green or explicitly waived.
Sent by Cursor Automation: Automated PR Review & Consolidation Agent
There was a problem hiding this comment.
Code Health Improved
(1 files improve in Code Health)
Fix Code Health degradations ℹ️
Gates Passed
4 Quality Gates Passed
View Improvements
| File | Code Health Impact | Categories Improved |
|---|---|---|
| main.py | 2.62 → 2.71 | Complex Method, Bumpy Road Ahead, Overall Code Complexity |
Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.


Summary
Closes ABHI-1347
What Changed and Why
validate_folder_url()so only trusted HTTPS sources are fetched before DNS/IP validation runs.allowed_blocklist_domainshandling in config loading/validation and surfaced the defaults inget_default_config().config.yaml.exampleandREADME.mdto document the allowlist.--folder-urlable to use config-defined allowlist domains, so explicit CLI URLs are still validated against operator-approved sources instead of being forced back to GitHub-only defaults.--folder-urlconfig allowlist path.How to Test
uv run pytest tests/ -vuv run ruff check .uv run pre-commit run --all-filesSecurity Implications
Link to Devin session: https://app.devin.ai/sessions/5e907c88304a40dc8d6b1cf7b1ba44c5
Requested by: @abhimehro