Skip to content
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

BED-5301/5298: PenTest Result: Internal IP Address Disclosure AND Rate limit Bypass #1163

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

ALCooper12
Copy link
Contributor

@ALCooper12 ALCooper12 commented Feb 21, 2025

Description

  • Replaced the tollbooth library/module with https://github.com/ulule/limiter in order to implement our current rate limiting middleware logic
  • I also implemented a new middleware specifically for the /api/v2/login endpoint in order to address part of the requirements for BED-5300

Motivation and Context

This PR addresses:

  • BED-5301
  • BED-5298
  • A little bit of BED-5300

Why is this change required? What problem does it solve?

  • This enables BHCE to follow stronger security practices, mitigate risks, and contribute to an overall improved security posture

How Has This Been Tested?

  • Local testing within the UI and terminal

Screenshots (optional):

UI
Screenshot 2025-02-27 at 3 44 11 PM

Terminal
Screenshot 2025-02-21 at 2 40 26 PM

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

…ementing our current rate limiting middleware logic
@ALCooper12 ALCooper12 added the bug Something isn't working label Feb 21, 2025
@ALCooper12 ALCooper12 self-assigned this Feb 21, 2025
@ALCooper12 ALCooper12 added the work in progress This pull request is a work in progress and should not be merged label Feb 21, 2025
@ALCooper12 ALCooper12 removed the work in progress This pull request is a work in progress and should not be merged label Feb 25, 2025
Comment on lines 38 to 55
ipGetter := stdlib.WithKeyGetter(
func(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff == "" {
slog.DebugContext(r.Context(), "No data found in X-Forwarded-For header")
return r.RemoteAddr
} else {
ips := strings.Split(xff, ",")
rightMostIP := strings.TrimSpace(ips[len(ips)-1])

return rightMostIP
}
},
)

middleware := stdlib.NewMiddleware(limiter, ipGetter)

return middleware.Handler

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to figure out why we couldn't use the reference X Forwarded For implementation from limiter but stumbled on the fact that you'd expect the leftmost to be the IP, but because we don't have multiple reverse proxies and the loadbalancer doesn't strip incoming X Forwarded For (in our case, almost always spoofed IPs), we can't rely on it. However, this puts us in a weird position because if someone is self hosting BloodHound, this will make it so anyone hosting with multiple reverse proxies/load balancers in front of them will end up detecting internal IPs for rate limiting.

I don't know if there's a one size fits all solution here, but we should probably consider the consequences before moving forward.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't solely rely on the left-most IP because of spoofing but if we take the rightmost IP we're basically rate limiting all traffic that comes in via the ALB (not good).

In a production deployment we only expect there to be 1 proxy in the X-Forwarded-For chain. It's not a perfect solution but I think it's sufficient for rate limiting at the service level:

  • If there is no X-Forwarded-For header then limit by request.RemoteAddr
  • If there are exactly 2 IPs in the X-Forwarded-For chain then we can limit by the left-most IP since the only 2 IPs should be the real client ID and the ALB proxy IP.
  • If there are more than 2 IPs in the X-Forwarded-For chain then there are more proxies in the chain than we expect (likely spoofing) and we can limit on the second to last IP because it is the untrusted proxy right before it hits the ALB.

Mind you, this isn't perfect but anything more sophisticated than this requires additional configuration on the ALB and/or external services.

idxIP = 0
}

return strings.TrimSpace(ips[idxIP])
Copy link
Contributor

@ddlees ddlees Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of just the ALB in the chain this will limit on the ip of the ALB and not the client ip.

len(ips) == 2
cfg.TrustedProxies == 1
len(ips) - cfg.TrustedProxies == 1

ips[1] = "ALB IP"

@@ -166,6 +166,7 @@ type Configuration struct {
FedRAMPEULAText string `json:"fedramp_eula_text"` // Enterprise only
EnableTextLogger bool `json:"enable_text_logger"`
RecreateDefaultAdmin bool `json:"recreate_default_admin"`
TrustedProxies int `json:"trusted_proxies"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this configuration value and can assume only 1 reverse proxy for the majority of cases. Otherwise, we might consider defaulting to 1 instead because coordinating configuration changes for production deployments is painful.

Copy link
Contributor

@ddlees ddlees left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly good but we need to revise IP selection a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants