-
Notifications
You must be signed in to change notification settings - Fork 3.7k
feat: make webhook SSRF protection configurable for self-hosted deployments #8732
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: preview
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| # See the LICENSE file for details. | ||
|
|
||
| # Python imports | ||
| import os | ||
| import socket | ||
| import ipaddress | ||
| from urllib.parse import urlparse | ||
|
|
@@ -15,6 +16,8 @@ | |
| from plane.db.models import Webhook, WebhookLog | ||
| from plane.db.models.webhook import validate_domain, validate_schema | ||
|
|
||
| ENABLE_WEBHOOK_SSRF_PROTECTION = os.environ.get("ENABLE_WEBHOOK_SSRF_PROTECTION", "1") != "0" | ||
|
|
||
|
|
||
| class WebhookSerializer(DynamicBaseSerializer): | ||
| url = serializers.URLField(validators=[validate_schema, validate_domain]) | ||
|
|
@@ -36,10 +39,11 @@ def create(self, validated_data): | |
| if not ip_addresses: | ||
| raise serializers.ValidationError({"url": "No IP addresses found for the hostname."}) | ||
|
|
||
| for addr in ip_addresses: | ||
| ip = ipaddress.ip_address(addr[4][0]) | ||
| if ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local: | ||
| raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."}) | ||
| if ENABLE_WEBHOOK_SSRF_PROTECTION: | ||
| for addr in ip_addresses: | ||
| ip = ipaddress.ip_address(addr[4][0]) | ||
| if ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local: | ||
| raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."}) | ||
|
Comment on lines
+42
to
+46
|
||
|
|
||
| # Additional validation for multiple request domains and their subdomains | ||
| request = self.context.get("request") | ||
|
|
@@ -71,10 +75,11 @@ def update(self, instance, validated_data): | |
| if not ip_addresses: | ||
| raise serializers.ValidationError({"url": "No IP addresses found for the hostname."}) | ||
|
|
||
| for addr in ip_addresses: | ||
| ip = ipaddress.ip_address(addr[4][0]) | ||
| if ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local: | ||
| raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."}) | ||
| if ENABLE_WEBHOOK_SSRF_PROTECTION: | ||
| for addr in ip_addresses: | ||
| ip = ipaddress.ip_address(addr[4][0]) | ||
| if ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local: | ||
| raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."}) | ||
|
|
||
| # Additional validation for multiple request domains and their subdomains | ||
| request = self.context.get("request") | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||||||||||||||||||
| # See the LICENSE file for details. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Python imports | ||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||
| from uuid import uuid4 | ||||||||||||||||||||||||||
| from urllib.parse import urlparse | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -27,8 +28,9 @@ def validate_schema(value): | |||||||||||||||||||||||||
| def validate_domain(value): | ||||||||||||||||||||||||||
| parsed_url = urlparse(value) | ||||||||||||||||||||||||||
| domain = parsed_url.netloc | ||||||||||||||||||||||||||
| if domain in ["localhost", "127.0.0.1"]: | ||||||||||||||||||||||||||
| raise ValidationError("Local URLs are not allowed.") | ||||||||||||||||||||||||||
| if os.environ.get("ENABLE_WEBHOOK_SSRF_PROTECTION", "1") != "0": | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| if domain in ["localhost", "127.0.0.1"]: | ||||||||||||||||||||||||||
| raise ValidationError("Local URLs are not allowed.") | ||||||||||||||||||||||||||
|
Comment on lines
29
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result:
So yes: Sources: Python Use The current validator compares the raw
The Suggested fix def validate_domain(value):
parsed_url = urlparse(value)
- domain = parsed_url.netloc
+ domain = parsed_url.hostname
if os.environ.get("ENABLE_WEBHOOK_SSRF_PROTECTION", "1") != "0":
- if domain in ["localhost", "127.0.0.1"]:
+ if domain in {"localhost", "127.0.0.1"}:
raise ValidationError("Local URLs are not allowed.")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| class Webhook(BaseModel): | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
ENABLE_WEBHOOK_SSRF_PROTECTIONenvironment variable is not documented in.env.example, whereas all other env vars (likeDEBUG,USE_MINIO,ENABLE_READ_REPLICA,API_KEY_RATE_LIMIT, etc.) are listed there. Without a corresponding entry, self-hosted operators who consult.env.exampleas the reference for available configuration options will not discover this new security toggle.An entry with a comment explaining its purpose and the default value (
1= protection enabled,0= disabled) should be added to.env.example.