Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion lib/request-processor/handle_urls.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
- direct SSRF attacks (hostname that resolves directly to a local IP address - does not go through redirects)
- direct IMDS SSRF attacks (hostname is an IMDS IP)
- blocked outbound domains (based on cloud configuration)
- redirect-based SSRF via domain allowlisting (prevents requests to non-allowlisted domains)

All these checks first verify if the hostname was provided via user input.
Protects both curl and fopen wrapper functions (file_get_contents, etc...).
Expand All @@ -42,7 +43,15 @@ func OnPreOutgoingRequest(instance *instance.RequestProcessorInstance) string {
return ""
}

res := ssrf.CheckContextForSSRF(instance, hostname, port, operation)
// Check domain allowlist for stream wrappers to prevent SSRF via redirects
// Stream wrappers (file_get_contents, fopen, file) cannot provide effective URL after redirects,
// so we require explicit domain allowlisting when the URL is user-controlled
res := ssrf.CheckDomainAllowlistForStreamWrappers(instance, hostname, port, operation)
if res != nil {
return attack.ReportAttackDetected(res, instance)
}

res = ssrf.CheckContextForSSRF(instance, hostname, port, operation)
if res != nil {
return attack.ReportAttackDetected(res, instance)
}
Expand Down
99 changes: 99 additions & 0 deletions lib/request-processor/vulnerabilities/ssrf/checkDomainAllowlist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package ssrf

import (
. "main/aikido_types"
"main/context"
"main/helpers"
"main/instance"
"main/utils"
)

// allowedDomains defines the list of domains that are allowed for outgoing HTTP requests
// from PHP stream wrappers (file_get_contents, fopen, file) when the URL is user-controlled.
// This prevents SSRF attacks via redirects to internal addresses, which cannot be detected
// for stream wrappers (unlike curl which provides the effective URL after redirects).
var allowedDomains = []string{"example.com"} // add your allowed domains here

// CheckDomainAllowlistForStreamWrappers validates that the requested hostname is in the allowlist
// for PHP stream wrapper functions that cannot detect redirects.
// This is the primary defense against redirect-based SSRF for stream wrappers.
// Returns an InterceptorResult if the domain is not allowed, nil otherwise.
func CheckDomainAllowlistForStreamWrappers(instance *instance.RequestProcessorInstance, hostname string, port uint32, operation string) *utils.InterceptorResult {
// Only apply this check to stream wrapper functions that cannot detect redirects
// curl is handled separately and can detect redirects via CURLINFO_EFFECTIVE_URL
isStreamWrapper := operation == "file" || operation == "file_get_contents" || operation == "fopen"
if !isStreamWrapper {
return nil
}

// Check if hostname was provided via user input
hostnameFoundInUserInput := false
var matchedSource string
var matchedPath string
var matchedPayload string

for _, source := range context.SOURCES {
mapss := source.CacheGet(instance)
for str, path := range mapss {
// Use the same hostname matching logic as findHostnameInUserInput
if findHostnameInUserInput(str, hostname, port) {
hostnameFoundInUserInput = true
matchedSource = source.Name
matchedPath = path
matchedPayload = str
break
}
}
if hostnameFoundInUserInput {
break
}
}

// If hostname was not found in user input, no SSRF risk
if !hostnameFoundInUserInput {
return nil
}

// Normalize the hostname for comparison
normalizedHostname := helpers.NormalizeHostname(hostname)

// Check if domain is in the allowlist (exact match only)
for _, allowedDomain := range allowedDomains {
normalizedAllowed := helpers.NormalizeHostname(allowedDomain)
if normalizedHostname == normalizedAllowed {
// Domain is explicitly allowed
return nil
}
}

// Check if domain is explicitly allowed in cloud configuration
var server *ServerData
if instance != nil {
server = instance.GetCurrentServer()
}
if server != nil {
server.CloudConfigMutex.Lock()
block, found := server.CloudConfig.OutboundDomains[normalizedHostname]
server.CloudConfigMutex.Unlock()

if found && !block {
// Domain is explicitly allowed in cloud config
return nil
}
}

// Domain is not in allowlist - block the request to prevent redirect-based SSRF
return &utils.InterceptorResult{
Operation: operation,
Kind: utils.Ssrf,
Source: matchedSource,
PathToPayload: matchedPath,
Metadata: map[string]string{
"hostname": hostname,
"blockedByAllowlist": "true",
"reason": "redirect_protection",
"streamWrapperFunction": operation,
},
Payload: matchedPayload,
}
}
Loading