Summary
Several features use OpenProject.httpx to make HTTP requests to admin-configured URLs without SSRF protection. This allows an admin user to make the server issue requests to internal network services, cloud metadata endpoints, and localhost services.
This is particularly notable because OpenProject has a dedicated OpenProject::SsrfProtection module (inheriting from the ssrf_filter gem) that properly blocks private IPs — but it is only used for outgoing webhooks. The Jira import, Nextcloud storage validation, OIDC metadata fetching, and SAML metadata fetching all use the unprotected OpenProject.httpx client directly.
Severity
CVSS 3.1: 6.5 (Medium) — CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N
Requires admin access, but in cloud-hosted environments (AWS/GCP/Azure), SSRF to instance metadata endpoints can yield IAM credentials.
Affected Components
1. Jira Import (app/services/import/jira_client.rb:49-66)
The JiraClient sends requests with Bearer auth to any URL:
@httpx = OpenProject
.httpx
.plugin(:auth)
.bearer_auth(personal_access_token) # Token sent to ANY target
URL validation only checks scheme (URI::HTTP or URI::HTTPS), not IP range.
2. Nextcloud Storage (modules/storages/app/validator/nextcloud_compatible_host_validator.rb:45-51)
Uses OpenProject.httpx to validate Nextcloud hosts. Additionally, secure_context_uri_validator.rb:55-66 explicitly allows http://localhost and *.localhost.
3. OIDC Metadata (modules/openid_connect/app/services/openid_connect/providers/update_service.rb:62)
Fetches OIDC discovery metadata from admin-supplied URL without SSRF protection.
4. SAML Metadata (modules/auth_saml/app/services/saml/update_metadata_service.rb:74)
Fetches SAML metadata from admin-supplied URL without SSRF protection.
Contrast: Webhooks ARE protected
# modules/webhooks/app/services/webhooks/outgoing/request_webhook_service.rb:16
response = OpenProject::SsrfProtection.post(webhook.url, headers:, body:)
Proof of Concept
Verified on OpenProject v15.5.1 (Docker):
-
Set up a listener on port 9999 on the Docker host
-
Inside the container, ran:
response = OpenProject.httpx.get("http://172.23.0.1:9999/ssrf-test")
Result: HTTP 200, and the listener captured the full request including any Bearer auth headers.
-
Port scanning confirmed via error differentiation:
- Open port (PostgreSQL 5432):
EOFError (connection accepted then closed)
- Closed port:
ConnectionError (connection refused)
-
SsrfProtection properly blocks the same requests:
OpenProject::SsrfProtection.post("http://172.23.0.1:9999/test")
# => raises SsrfFilter::PrivateIPAddress
Root Cause Pattern
OpenProject.httpx is used in 15+ locations across the codebase. The SsrfProtection wrapper exists and works correctly but is only applied to webhooks. The fix is to use SsrfProtection (or equivalent IP range validation) for ALL server-side HTTP calls to admin-configured URLs.
Impact
- Internal network reconnaissance via port scanning
- Cloud instance metadata access (IAM credential theft on AWS/GCP/Azure)
- Bearer token exfiltration to attacker-controlled servers (Jira import)
- Bypass of network segmentation in multi-tier architectures
Suggested Fix
Replace OpenProject.httpx with OpenProject::SsrfProtection for all outgoing HTTP requests to user/admin-controlled URLs, or add IP range validation before making requests.
References
Best regards,
William Goodfellow
Summary
Several features use
OpenProject.httpxto make HTTP requests to admin-configured URLs without SSRF protection. This allows an admin user to make the server issue requests to internal network services, cloud metadata endpoints, and localhost services.This is particularly notable because OpenProject has a dedicated
OpenProject::SsrfProtectionmodule (inheriting from thessrf_filtergem) that properly blocks private IPs — but it is only used for outgoing webhooks. The Jira import, Nextcloud storage validation, OIDC metadata fetching, and SAML metadata fetching all use the unprotectedOpenProject.httpxclient directly.Severity
CVSS 3.1: 6.5 (Medium) — CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N
Requires admin access, but in cloud-hosted environments (AWS/GCP/Azure), SSRF to instance metadata endpoints can yield IAM credentials.
Affected Components
1. Jira Import (
app/services/import/jira_client.rb:49-66)The
JiraClientsends requests with Bearer auth to any URL:URL validation only checks scheme (
URI::HTTPorURI::HTTPS), not IP range.2. Nextcloud Storage (
modules/storages/app/validator/nextcloud_compatible_host_validator.rb:45-51)Uses
OpenProject.httpxto validate Nextcloud hosts. Additionally,secure_context_uri_validator.rb:55-66explicitly allowshttp://localhostand*.localhost.3. OIDC Metadata (
modules/openid_connect/app/services/openid_connect/providers/update_service.rb:62)Fetches OIDC discovery metadata from admin-supplied URL without SSRF protection.
4. SAML Metadata (
modules/auth_saml/app/services/saml/update_metadata_service.rb:74)Fetches SAML metadata from admin-supplied URL without SSRF protection.
Contrast: Webhooks ARE protected
Proof of Concept
Verified on OpenProject v15.5.1 (Docker):
Set up a listener on port 9999 on the Docker host
Inside the container, ran:
Result: HTTP 200, and the listener captured the full request including any Bearer auth headers.
Port scanning confirmed via error differentiation:
EOFError(connection accepted then closed)ConnectionError(connection refused)SsrfProtectionproperly blocks the same requests:Root Cause Pattern
OpenProject.httpxis used in 15+ locations across the codebase. TheSsrfProtectionwrapper exists and works correctly but is only applied to webhooks. The fix is to useSsrfProtection(or equivalent IP range validation) for ALL server-side HTTP calls to admin-configured URLs.Impact
Suggested Fix
Replace
OpenProject.httpxwithOpenProject::SsrfProtectionfor all outgoing HTTP requests to user/admin-controlled URLs, or add IP range validation before making requests.References
lib/open_project/ssrf_protection.rbBest regards,
William Goodfellow