diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index 357cc80..36bc280 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -553,6 +553,9 @@ func (m *ProxyHandler) prepareRequestWithoutSession(req *http.Request, reqCtx *R if hostConfig.Rewrite != nil { for _, replacement := range hostConfig.Rewrite { if replacement.From == "" || replacement.From == "request_body" || replacement.From == "any" { + if !m.rewriteRuleMatchesRequest(replacement, req) { + continue + } engine := replacement.Engine if engine == "" { engine = "regex" @@ -949,7 +952,9 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithVariables(resp * if hCfg.Rewrite != nil { for _, replacement := range hCfg.Rewrite { if replacement.From == "response_header" || replacement.From == "any" { - headers = m.applyReplacementWithVariables(headers, replacement, session.ID, varCtx, "") + if m.rewriteRuleMatchesRequest(replacement, resp.Request) { + headers = m.applyReplacementWithVariables(headers, replacement, session.ID, varCtx, "") + } } } } @@ -959,7 +964,9 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithVariables(resp * if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil { for _, replacement := range proxyConfig.Global.Rewrite { if replacement.From == "response_header" || replacement.From == "any" { - headers = m.applyReplacementWithVariables(headers, replacement, session.ID, varCtx, "") + if m.rewriteRuleMatchesRequest(replacement, resp.Request) { + headers = m.applyReplacementWithVariables(headers, replacement, session.ID, varCtx, "") + } } } } @@ -1011,7 +1018,7 @@ func (m *ProxyHandler) rewriteResponseBodyWithContext(resp *http.Response, reqCt // build variables context for template interpolation varCtx := m.buildVariablesContext(resp.Request.Context(), reqCtx.Session, reqCtx.ProxyConfig) - body = m.applyCustomReplacementsWithVariables(body, reqCtx.Session, reqCtx.TargetDomain, reqCtx.ProxyConfig, varCtx, contentType) + body = m.applyCustomReplacementsWithVariables(body, reqCtx.Session, reqCtx.TargetDomain, reqCtx.ProxyConfig, varCtx, contentType, resp.Request) // apply obfuscation if enabled if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") { @@ -1152,7 +1159,7 @@ func (m *ProxyHandler) rewriteResponseBodyWithoutSessionContext(resp *http.Respo body = m.patchUrls(configMap, body, CONVERT_TO_PHISHING_URLS) body = m.applyURLPathRewritesWithoutSession(body, reqCtx) - body = m.applyCustomReplacementsWithoutSession(body, configMap, reqCtx.TargetDomain, reqCtx.ProxyConfig, contentType) + body = m.applyCustomReplacementsWithoutSession(body, configMap, reqCtx.TargetDomain, reqCtx.ProxyConfig, contentType, resp.Request) // apply obfuscation if enabled if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") { @@ -2352,7 +2359,9 @@ func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Reque if hCfg.Rewrite != nil { for _, replacement := range hCfg.Rewrite { if replacement.From == "" || replacement.From == "request_body" || replacement.From == "any" { - body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, "") + if m.rewriteRuleMatchesRequest(replacement, req) { + body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, "") + } } } } @@ -2362,7 +2371,9 @@ func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Reque if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil { for _, replacement := range proxyConfig.Global.Rewrite { if replacement.From == "" || replacement.From == "request_body" || replacement.From == "any" { - body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, "") + if m.rewriteRuleMatchesRequest(replacement, req) { + body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, "") + } } } } @@ -2370,18 +2381,20 @@ func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Reque req.Body = io.NopCloser(bytes.NewBuffer(body)) } -func (m *ProxyHandler) applyCustomReplacements(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML) []byte { - return m.applyCustomReplacementsWithVariables(body, session, targetDomain, proxyConfig, nil, "") +func (m *ProxyHandler) applyCustomReplacements(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, req *http.Request) []byte { + return m.applyCustomReplacementsWithVariables(body, session, targetDomain, proxyConfig, nil, "", req) } -func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, varCtx *VariablesContext, contentType string) []byte { +func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, varCtx *VariablesContext, contentType string, req *http.Request) []byte { // apply rewrite rules for the current request's target domain if hostConfig, ok := session.Config.Load(targetDomain); ok { hCfg := hostConfig.(service.ProxyServiceDomainConfig) if hCfg.Rewrite != nil { for _, replacement := range hCfg.Rewrite { if replacement.From == "" || replacement.From == "response_body" || replacement.From == "any" { - body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, contentType) + if m.rewriteRuleMatchesRequest(replacement, req) { + body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, contentType) + } } } } @@ -2391,7 +2404,9 @@ func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil { for _, replacement := range proxyConfig.Global.Rewrite { if replacement.From == "" || replacement.From == "response_body" || replacement.From == "any" { - body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, contentType) + if m.rewriteRuleMatchesRequest(replacement, req) { + body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx, contentType) + } } } } @@ -2411,7 +2426,9 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithoutSession(resp if hostConfig.Rewrite != nil { for _, replacement := range hostConfig.Rewrite { if replacement.From == "response_header" || replacement.From == "any" { - headers = m.applyReplacement(headers, replacement, "no-session", "") + if m.rewriteRuleMatchesRequest(replacement, resp.Request) { + headers = m.applyReplacement(headers, replacement, "no-session", "") + } } } } @@ -2421,7 +2438,9 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithoutSession(resp if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil { for _, replacement := range proxyConfig.Global.Rewrite { if replacement.From == "response_header" || replacement.From == "any" { - headers = m.applyReplacement(headers, replacement, "no-session", "") + if m.rewriteRuleMatchesRequest(replacement, resp.Request) { + headers = m.applyReplacement(headers, replacement, "no-session", "") + } } } } @@ -2446,13 +2465,15 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithoutSession(resp } } -func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config map[string]service.ProxyServiceDomainConfig, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, contentType string) []byte { +func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config map[string]service.ProxyServiceDomainConfig, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, contentType string, req *http.Request) []byte { // apply rewrite rules for the current target domain if hostConfig, ok := config[targetDomain]; ok { if hostConfig.Rewrite != nil { for _, replacement := range hostConfig.Rewrite { if replacement.From == "" || replacement.From == "response_body" || replacement.From == "any" { - body = m.applyReplacement(body, replacement, "no-session", contentType) + if m.rewriteRuleMatchesRequest(replacement, req) { + body = m.applyReplacement(body, replacement, "no-session", contentType) + } } } } @@ -2462,7 +2483,9 @@ func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config if proxyConfig != nil && proxyConfig.Global != nil && proxyConfig.Global.Rewrite != nil { for _, replacement := range proxyConfig.Global.Rewrite { if replacement.From == "" || replacement.From == "response_body" || replacement.From == "any" { - body = m.applyReplacement(body, replacement, "no-session", contentType) + if m.rewriteRuleMatchesRequest(replacement, req) { + body = m.applyReplacement(body, replacement, "no-session", contentType) + } } } } @@ -2470,6 +2493,28 @@ func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config return body } +// rewriteRuleMatchesRequest returns true if the rewrite rule's path/method constraints +// match the given request. If no constraints are set the rule always matches. +// If req is nil (no request context available), rules with path/method constraints are skipped. +func (m *ProxyHandler) rewriteRuleMatchesRequest(rule service.ProxyServiceReplaceRule, req *http.Request) bool { + if req == nil { + return rule.Path == "" && rule.Method == "" + } + if rule.Method != "" && !strings.EqualFold(rule.Method, req.Method) { + return false + } + if rule.Path != "" { + if rule.PathRe == nil { + // path was set but failed to compile — skip rule safely + return false + } + if !rule.PathRe.MatchString(req.URL.Path) { + return false + } + } + return true +} + func (m *ProxyHandler) applyReplacement(body []byte, replacement service.ProxyServiceReplaceRule, sessionID string, contentType string) []byte { return m.applyReplacementWithVariables(body, replacement, sessionID, nil, contentType) } @@ -2898,6 +2943,9 @@ func (m *ProxyHandler) applyEarlyRequestHeaderReplacements(req *http.Request, re applyReplacements := func(replacements []service.ProxyServiceReplaceRule) { for _, replacement := range replacements { if replacement.From == "" || replacement.From == "request_header" || replacement.From == "any" { + if !m.rewriteRuleMatchesRequest(replacement, req) { + continue + } engine := replacement.Engine if engine == "" { engine = "regex" diff --git a/backend/service/proxy.go b/backend/service/proxy.go index d7fb168..2886a0d 100644 --- a/backend/service/proxy.go +++ b/backend/service/proxy.go @@ -160,7 +160,7 @@ func GetValidProxyVariableNames() []string { // - "public": Allow all traffic (traditional proxy mode) - on_deny is ignored // - "private": Strict IP-based mode like evilginx2 - whitelist IP after lure access, deny all others (DEFAULT) -// CompilePathPatterns compiles regex patterns for all capture and response rules +// CompilePathPatterns compiles regex patterns for all capture, response, and rewrite rules func CompilePathPatterns(config *ProxyServiceConfigYAML) error { // Compile global capture rule patterns if config.Global != nil && config.Global.Capture != nil { @@ -180,6 +180,15 @@ func CompilePathPatterns(config *ProxyServiceConfigYAML) error { } } + // Compile global rewrite rule patterns + if config.Global != nil && config.Global.Rewrite != nil { + for i := range config.Global.Rewrite { + if err := compileRewritePath(&config.Global.Rewrite[i]); err != nil { + return err + } + } + } + // Compile host-specific capture rule patterns for _, hostConfig := range config.Hosts { if hostConfig != nil && hostConfig.Capture != nil { @@ -201,6 +210,30 @@ func CompilePathPatterns(config *ProxyServiceConfigYAML) error { } } } + + // Compile host-specific rewrite rule patterns + for _, hostConfig := range config.Hosts { + if hostConfig != nil && hostConfig.Rewrite != nil { + for i := range hostConfig.Rewrite { + if err := compileRewritePath(&hostConfig.Rewrite[i]); err != nil { + return err + } + } + } + } + + return nil +} + +// compileRewritePath compiles the path pattern for a rewrite rule +func compileRewritePath(rule *ProxyServiceReplaceRule) error { + if rule.Path != "" { + pathRe, err := regexp.Compile(rule.Path) + if err != nil { + return fmt.Errorf("invalid regex pattern for rewrite path '%s': %w", rule.Path, err) + } + rule.PathRe = pathRe + } return nil } @@ -275,13 +308,16 @@ func (c *ProxyServiceCaptureRule) GetFindAsString() string { // ProxyServiceReplaceRule represents a replacement rule type ProxyServiceReplaceRule struct { - Name string `yaml:"name,omitempty"` - Engine string `yaml:"engine,omitempty"` // "regex" (default) or "dom" - Find string `yaml:"find,omitempty"` // regex pattern (regex engine) or css selector (dom engine) - Replace string `yaml:"replace,omitempty"` // replacement value for both engines - Action string `yaml:"action,omitempty"` // dom action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove - Target string `yaml:"target,omitempty"` // target matching: "first", "last", "all" (default), "1,3,5", "2-4" - From string `yaml:"from,omitempty"` + Name string `yaml:"name,omitempty"` + Engine string `yaml:"engine,omitempty"` // "regex" (default) or "dom" + Find string `yaml:"find,omitempty"` // regex pattern (regex engine) or css selector (dom engine) + Replace string `yaml:"replace,omitempty"` // replacement value for both engines + Action string `yaml:"action,omitempty"` // dom action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove + Target string `yaml:"target,omitempty"` // target matching: "first", "last", "all" (default), "1,3,5", "2-4" + From string `yaml:"from,omitempty"` + Path string `yaml:"path,omitempty"` // regex pattern to restrict rule to matching request paths + Method string `yaml:"method,omitempty"` // restrict rule to this HTTP method (e.g. GET, POST) + PathRe *regexp.Regexp `yaml:"-"` // compiled regex for path matching } // ProxyServiceURLRewriteRule represents a URL rewrite rule for anti-detection diff --git a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte index 12c1764..ff5584e 100644 --- a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte +++ b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte @@ -518,7 +518,16 @@ function addGlobalRewriteRule() { configData.global.rewrite = [ ...configData.global.rewrite, - { _id: getRuleId(), name: '', engine: 'regex', find: '', replace: '', from: 'response_body' } + { + _id: getRuleId(), + name: '', + engine: 'regex', + find: '', + replace: '', + from: 'response_body', + path: '', + method: '' + } ]; } @@ -576,7 +585,16 @@ function addHostRewriteRule(hostIndex) { configData.hosts[hostIndex].rewrite = [ ...(configData.hosts[hostIndex].rewrite || []), - { _id: getRuleId(), name: '', engine: 'regex', find: '', replace: '', from: 'response_body' } + { + _id: getRuleId(), + name: '', + engine: 'regex', + find: '', + replace: '', + from: 'response_body', + path: '', + method: '' + } ]; configData.hosts = [...configData.hosts]; } @@ -751,7 +769,7 @@ } function isRewriteRuleTouched(rule) { - return !!(rule.find?.trim() || rule.replace?.trim()); + return !!(rule.find?.trim() || rule.replace?.trim() || rule.path?.trim()); } function isResponseRuleTouched(rule) { @@ -1896,6 +1914,27 @@ Engine +