added trusted ip forward header to phishing server

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2026-05-27 22:36:10 +02:00
parent e83b5d0e2f
commit bff948c760
9 changed files with 73 additions and 57 deletions
+1
View File
@@ -206,6 +206,7 @@ func NewControllers(
CampaignService: services.Campaign,
ExecPath: conf.RemoteBrowser.ExecPath,
Enabled: conf.RemoteBrowser.Enabled,
TrustedProxies: conf.IPSecurity.TrustedProxies,
}
return &Controllers{
+11 -7
View File
@@ -63,6 +63,7 @@ type Server struct {
proxyServer *proxy.ProxyHandler
ja4Middleware *middleware.JA4Middleware
remoteBrowserWSPath string
trustedProxies []string
}
// NewServer returns a new server
@@ -76,6 +77,7 @@ func NewServer(
logger *zap.SugaredLogger,
certMagicConfig *certmagic.Config,
remoteBrowserWSPath string,
trustedProxies []string,
) *Server {
// setup ja4 middleware for tls fingerprinting
ja4Middleware := middleware.NewJA4Middleware(logger)
@@ -95,6 +97,7 @@ func NewServer(
services.IPAllowList,
repositories.Option,
services.Option,
trustedProxies,
)
// setup proxy session cleanup routine
@@ -121,6 +124,7 @@ func NewServer(
proxyServer: proxyServer,
ja4Middleware: ja4Middleware,
remoteBrowserWSPath: remoteBrowserWSPath,
trustedProxies: trustedProxies,
}
}
@@ -712,7 +716,7 @@ func (s *Server) checkAndServePhishingPage(
return false, fmt.Errorf("failed to get campaign template: %s", err)
}
// check that the requesters IP is allow listed
ip := utils.ExtractClientIP(c.Request)
ip := utils.ExtractClientIP(c.Request, s.trustedProxies)
servedByIPFilter, err := s.checkIPFilter(c, ip, campaign, domain, &campaignID)
if err != nil {
return false, err
@@ -990,7 +994,7 @@ func (s *Server) checkAndServePhishingPage(
submitDataEventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA]
newEventID := uuid.New()
campaignID := campaign.ID.MustGet()
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
submittedData := vo.NewEmptyOptionalString1MB()
@@ -1306,7 +1310,7 @@ func (s *Server) checkAndServePhishingPage(
// only create synthetic event if no message_read event exists
if !hasMessageRead {
syntheticReadEventID := uuid.New()
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
syntheticData := vo.NewOptionalString1MBMust("synthetic_from_page_visit")
@@ -1376,7 +1380,7 @@ func (s *Server) checkAndServePhishingPage(
eventName = data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED
}
eventID := cache.EventIDByName[eventName]
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
var visitEvent *model.CampaignEvent
if !campaign.IsAnonymous.MustGet() {
@@ -1669,7 +1673,7 @@ func (s *Server) checkAndServePhishingPage(
// only create synthetic event if no message_read event exists
if !hasMessageRead {
syntheticReadEventID := uuid.New()
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
syntheticData := vo.NewOptionalString1MBMust("synthetic_from_page_visit")
@@ -1738,7 +1742,7 @@ func (s *Server) checkAndServePhishingPage(
campaignEventID := cache.EventIDByName[eventName]
eventID := uuid.New()
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
var event *model.CampaignEvent
if !campaign.IsAnonymous.MustGet() {
@@ -1941,7 +1945,7 @@ func (s *Server) renderDenyPage(
// log deny page visited event
denyPageVisitEventID := uuid.New()
eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED]
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
var event *model.CampaignEvent
if !campaign.IsAnonymous.MustGet() {
+2
View File
@@ -59,6 +59,7 @@ func NewServices(
certMagicConfig *certmagic.Config,
certMagicCache *certmagic.Cache,
filePath string,
trustedProxies []string,
) *Services {
common := service.Common{
Logger: logger,
@@ -222,6 +223,7 @@ func NewServices(
MicrosoftDeviceCodeRepository: repositories.MicrosoftDeviceCode,
AttachmentPath: attachmentPath,
RemoteBrowserService: remoteBrowser,
TrustedProxies: trustedProxies,
}
// wire campaign service into microsoft device code service now that campaign is constructed
microsoftDeviceCodeService.CampaignService = campaign
+3 -2
View File
@@ -164,7 +164,8 @@ type RemoteBrowserController struct {
ExecPath string
// Enabled mirrors config.RemoteBrowserServerConfig.Enabled. When false
// every endpoint returns 404 and the feature is fully unavailable.
Enabled bool
Enabled bool
TrustedProxies []string
}
func (m *RemoteBrowserController) isEnabled(g *gin.Context) bool {
@@ -708,7 +709,7 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) {
}
}()
clientIP := utils.ExtractClientIP(g.Request)
clientIP := utils.ExtractClientIP(g.Request, m.TrustedProxies)
userAgent := g.Request.UserAgent()
// processEvent handles server-side effects for a RunEvent (DB writes, session state
+2
View File
@@ -231,6 +231,7 @@ func main() {
certMagicConfig,
certMagicCache,
*flagFilePath,
conf.IPSecurity.TrustedProxies,
)
// get entra-id options and setup msal client
ssoOpt, err := services.SSO.GetSSOOptionWithoutAuth(context.Background())
@@ -371,6 +372,7 @@ func main() {
logger,
certMagicConfig,
rbWSPath,
conf.IPSecurity.TrustedProxies,
)
var r *gin.Engine
+2 -28
View File
@@ -1,12 +1,11 @@
package middleware
import (
"net"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/phishingclub/phishingclub/config"
"github.com/phishingclub/phishingclub/utils"
"go.uber.org/zap"
)
@@ -17,33 +16,8 @@ func NewAllowIPMiddleware(conf *config.Config, logger *zap.SugaredLogger) gin.Ha
c.Next()
return
}
c.RemoteIP()
clientIP := c.ClientIP()
allowed := false
for _, allowedIP := range conf.IPSecurity.AdminAllowed {
// check if the allowed entry is a CIDR
if strings.Contains(allowedIP, "/") {
_, ipNet, err := net.ParseCIDR(allowedIP)
if err != nil {
logger.Errorw("Invalid CIDR in allowed IPs",
"cidr", allowedIP,
"error", err)
continue
}
ip := net.ParseIP(clientIP)
if ipNet.Contains(ip) {
allowed = true
break
}
} else {
// Direct IP comparison
if clientIP == allowedIP {
allowed = true
break
}
}
}
allowed := utils.IPMatchesList(clientIP, conf.IPSecurity.AdminAllowed)
if !allowed {
logger.Infow("blocked unauthorized IP access attempt",
+10 -7
View File
@@ -125,6 +125,7 @@ type ProxyHandler struct {
OptionRepository *repository.Option
OptionService *service.Option
cookieName string
trustedProxies []string
}
func NewProxyHandler(
@@ -142,6 +143,7 @@ func NewProxyHandler(
ipAllowListService *service.IPAllowListService,
optionRepo *repository.Option,
optionService *service.Option,
trustedProxies []string,
) *ProxyHandler {
// get proxy cookie name from database
cookieName := "ps" // fallback default
@@ -165,6 +167,7 @@ func NewProxyHandler(
OptionRepository: optionRepo,
OptionService: optionService,
cookieName: cookieName,
trustedProxies: trustedProxies,
}
}
@@ -3612,7 +3615,7 @@ func (m *ProxyHandler) createCampaignInfoEvent(session *service.ProxySession, ca
}
eventID := uuid.New()
clientIP := utils.ExtractClientIP(req)
clientIP := utils.ExtractClientIP(req, m.trustedProxies)
metadata := model.ExtractCampaignEventMetadataFromHTTPRequest(req, campaign)
event := &model.CampaignEvent{
@@ -3682,7 +3685,7 @@ func (m *ProxyHandler) createCampaignSubmitEvent(session *service.ProxySession,
eventID := uuid.New()
// use the event creation below instead of service call
clientIP := utils.ExtractClientIP(req)
clientIP := utils.ExtractClientIP(req, m.trustedProxies)
metadata := model.ExtractCampaignEventMetadataFromHTTPRequest(req, campaign)
@@ -4305,7 +4308,7 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *servic
// only create synthetic event if no message_read event exists
if !hasMessageRead {
syntheticReadEventID := uuid.New()
clientIP := utils.ExtractClientIP(req)
clientIP := utils.ExtractClientIP(req, m.trustedProxies)
clientIPVO := vo.NewOptionalString64Must(clientIP)
userAgent := vo.NewOptionalString255Must(utils.Substring(session.UserAgent, 0, 255))
syntheticData := vo.NewOptionalString1MBMust("synthetic_from_page_visit")
@@ -4384,7 +4387,7 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *servic
// create visit event
visitEventID := uuid.New()
clientIP := utils.ExtractClientIP(req)
clientIP := utils.ExtractClientIP(req, m.trustedProxies)
clientIPVO := vo.NewOptionalString64Must(clientIP)
userAgent := vo.NewOptionalString255Must(utils.Substring(session.UserAgent, 0, 255))
@@ -5022,7 +5025,7 @@ func (m *ProxyHandler) registerDenyPageVisitEventDirect(req *http.Request, reqCt
eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED]
newEventID := uuid.New()
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req))
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req, m.trustedProxies))
userAgent := vo.NewOptionalString255Must(utils.Substring(reqCtx.OriginalUserAgent, 0, 1000)) // MAX_USER_AGENT_SAVED equivalent
var event *model.CampaignEvent
@@ -5096,7 +5099,7 @@ func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, re
eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED]
newEventID := uuid.New()
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req))
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req, m.trustedProxies))
userAgent := vo.NewOptionalString255Must(utils.Substring(reqCtx.OriginalUserAgent, 0, 1000)) // MAX_USER_AGENT_SAVED equivalent
var event *model.CampaignEvent
@@ -5158,7 +5161,7 @@ func (m *ProxyHandler) checkFilter(req *http.Request, reqCtx *RequestContext) (b
campaignID := reqCtx.CampaignID
// extract client IP and strip port if present using net.SplitHostPort for IPv6 safety
ip := utils.ExtractClientIP(req)
ip := utils.ExtractClientIP(req, m.trustedProxies)
if host, _, err := net.SplitHostPort(ip); err == nil {
ip = host
}
+2 -1
View File
@@ -58,6 +58,7 @@ type Campaign struct {
MicrosoftDeviceCodeRepository *repository.MicrosoftDeviceCode
AttachmentPath string
RemoteBrowserService *RemoteBrowser
TrustedProxies []string
}
// Create creates a new campaign
@@ -1327,7 +1328,7 @@ func (c *Campaign) SaveTrackingPixelLoaded(
Metadata: vo.NewEmptyOptionalString1MB(),
}
} else {
ip := vo.NewOptionalString64Must(utils.ExtractClientIP(ctx.Request))
ip := vo.NewOptionalString64Must(utils.ExtractClientIP(ctx.Request, c.TrustedProxies))
ua := ctx.Request.UserAgent()
if len(ua) > 255 {
ua = strings.TrimSpace(ua[:255])
+40 -12
View File
@@ -6,12 +6,22 @@ import (
"strings"
)
// ExtractClientIP extracts the real client IP from an HTTP request,
// checking common proxy headers in order of preference.
// This provides consistent IP extraction across the application.
func ExtractClientIP(req *http.Request) string {
// ExtractClientIP extracts the real client IP from an HTTP request.
// trustedProxies is a list of proxy IPs/CIDRs whose forwarded headers should
// be trusted. When empty all forwarded headers are trusted (legacy behaviour).
// When non-empty, forwarded headers are only honoured if RemoteAddr is in the
// trusted list; otherwise RemoteAddr is returned directly.
func ExtractClientIP(req *http.Request, trustedProxies []string) string {
// start with direct connection IP
clientIP := req.RemoteAddr
// strip port for comparison and return value
if host, _, err := net.SplitHostPort(clientIP); err == nil {
clientIP = host
}
if len(trustedProxies) > 0 && !IPMatchesList(clientIP, trustedProxies) {
return clientIP
}
// check common proxy headers in order of preference
proxyHeaders := []string{
@@ -29,18 +39,36 @@ func ExtractClientIP(req *http.Request) string {
// take first IP if comma-separated list
ip := strings.SplitN(headerValue, ",", 2)[0]
ip = strings.TrimSpace(ip)
// use first non-empty value found
if ip != "" {
clientIP = ip
break
if host, _, err := net.SplitHostPort(ip); err == nil {
return host
}
return ip
}
}
}
// strip port
if host, _, err := net.SplitHostPort(clientIP); err == nil {
clientIP = host
}
return clientIP
}
// IPMatchesList reports whether ip matches any entry in the list, which may
// contain plain IPs or CIDR ranges.
func IPMatchesList(ip string, list []string) bool {
parsed := net.ParseIP(ip)
for _, entry := range list {
if strings.Contains(entry, "/") {
_, ipNet, err := net.ParseCIDR(entry)
if err != nil {
continue
}
if parsed != nil && ipNet.Contains(parsed) {
return true
}
} else {
if ip == entry {
return true
}
}
}
return false
}