From 78b2e57b14ffb3028a6d0c04a54e45f9f5928375 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 5 Feb 2026 01:59:43 +0100 Subject: [PATCH] Fix proxy bugs with domain rewrite outside scope, global rewrite more consistent, fix dublicate global rewrite, add global rewrite to body Fix proxy replace without from too eager Fix redundant proxy header rewrite Fix multiline proxy yaml, do not format on backend --- backend/proxy/proxy.go | 117 ++++++++---------- backend/service/proxy.go | 6 - .../proxy/ProxyConfigBuilder.svelte | 22 +--- frontend/src/lib/components/yaml/index.js | 89 +++++++++++++ frontend/src/routes/proxy/+page.svelte | 23 +--- 5 files changed, 154 insertions(+), 103 deletions(-) diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index a2032a1..3e36cd9 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -539,46 +539,14 @@ func (m *ProxyHandler) prepareRequestWithoutSession(req *http.Request, reqCtx *R } } - // append global rewrite rules to host config for requests without session + // note: header rewrite rules are already applied in applyEarlyRequestHeaderReplacements + + // apply body rewrite rules (no capture) + // append global rewrite rules to host config for body replacements if reqCtx.ProxyConfig != nil && reqCtx.ProxyConfig.Global != nil && reqCtx.ProxyConfig.Global.Rewrite != nil { hostConfig.Rewrite = append(hostConfig.Rewrite, reqCtx.ProxyConfig.Global.Rewrite...) } - // apply header rewrite rules (no capture) - if hostConfig.Rewrite != nil { - for _, replacement := range hostConfig.Rewrite { - if replacement.From == "" || replacement.From == "request_header" || replacement.From == "any" { - engine := replacement.Engine - if engine == "" { - engine = "regex" - } - if engine == "regex" { - re, err := regexp.Compile(replacement.Find) - if err != nil { - continue - } - for headerName, values := range req.Header { - newValues := make([]string, 0, len(values)) - for _, val := range values { - fullHeader := headerName + ": " + val - replaced := re.ReplaceAllString(fullHeader, replacement.Replace) - if strings.HasPrefix(replaced, headerName+": ") { - newVal := replaced[len(headerName)+2:] - newValues = append(newValues, newVal) - } else if replaced != fullHeader { - newValues = append(newValues, val) - } else { - newValues = append(newValues, val) - } - } - req.Header[headerName] = newValues - } - } - } - } - } - - // apply body rewrite rules (no capture) if req.Body != nil { body, err := io.ReadAll(req.Body) if err == nil { @@ -1034,7 +1002,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, varCtx, contentType) + body = m.applyCustomReplacementsWithVariables(body, reqCtx.Session, reqCtx.TargetDomain, reqCtx.ProxyConfig, varCtx, contentType) // apply obfuscation if enabled if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") { @@ -1117,14 +1085,13 @@ func (m *ProxyHandler) createMinimalConfig(phishDomain, targetDomain string) map config[targetDomain] = service.ProxyServiceDomainConfig{To: phishDomain} } - // add global rules to all host configurations + // add global capture rules to all host configurations + // note: global rewrite rules are applied separately in applyCustomReplacementsWithoutSession if fullConfigYAML != nil && fullConfigYAML.Global != nil { for originalHost := range config { hostConfig := config[originalHost] // append global capture rules hostConfig.Capture = append(hostConfig.Capture, fullConfigYAML.Global.Capture...) - // append global rewrite rules - hostConfig.Rewrite = append(hostConfig.Rewrite, fullConfigYAML.Global.Rewrite...) config[originalHost] = hostConfig } } @@ -1175,7 +1142,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, contentType) + body = m.applyCustomReplacementsWithoutSession(body, configMap, reqCtx.TargetDomain, reqCtx.ProxyConfig, contentType) // apply obfuscation if enabled if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") { @@ -1412,13 +1379,12 @@ func (m *ProxyHandler) buildSessionConfig(targetDomain, phishDomain string, prox } } - // add global rules only to the target domain configuration + // add global capture rules only to the target domain configuration + // note: global rewrite rules are applied separately in applyCustomReplacementsWithVariables if proxyConfig.Global != nil && sessionConfig[targetDomain].To != "" { hostConfig := sessionConfig[targetDomain] // append global capture rules hostConfig.Capture = append(hostConfig.Capture, proxyConfig.Global.Capture...) - // append global rewrite rules - hostConfig.Rewrite = append(hostConfig.Rewrite, proxyConfig.Global.Rewrite...) sessionConfig[targetDomain] = hostConfig } @@ -1494,7 +1460,7 @@ func (m *ProxyHandler) onRequestBody(req *http.Request, session *service.ProxySe // build variables context using the proxy config passed from caller varCtx := m.buildVariablesContext(req.Context(), session, proxyConfig) - m.applyRequestBodyReplacementsWithVariables(req, session, varCtx) + m.applyRequestBodyReplacementsWithVariables(req, session, proxyConfig, varCtx) } func (m *ProxyHandler) onRequestHeader(req *http.Request, session *service.ProxySession) { @@ -2342,18 +2308,18 @@ func (m *ProxyHandler) createCookieBundle(cookieCaptures map[string]map[string]s return bundledData } -func (m *ProxyHandler) applyRequestBodyReplacements(req *http.Request, session *service.ProxySession) { - m.applyRequestBodyReplacementsWithVariables(req, session, nil) +func (m *ProxyHandler) applyRequestBodyReplacements(req *http.Request, session *service.ProxySession, proxyConfig *service.ProxyServiceConfigYAML) { + m.applyRequestBodyReplacementsWithVariables(req, session, proxyConfig, nil) } -func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Request, session *service.ProxySession, varCtx *VariablesContext) { +func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Request, session *service.ProxySession, proxyConfig *service.ProxyServiceConfigYAML, varCtx *VariablesContext) { if req.Body == nil { return } body := m.readRequestBody(req) - // only apply rewrite rules for the current host + // apply rewrite rules for the current host if hostConfig, ok := session.Config.Load(req.Host); ok { hCfg := hostConfig.(service.ProxyServiceDomainConfig) if hCfg.Rewrite != nil { @@ -2365,16 +2331,25 @@ func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Reque } } + // apply global rewrite rules + 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, "") + } + } + } + req.Body = io.NopCloser(bytes.NewBuffer(body)) } -func (m *ProxyHandler) applyCustomReplacements(body []byte, session *service.ProxySession) []byte { - return m.applyCustomReplacementsWithVariables(body, session, nil, "") +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) applyCustomReplacementsWithVariables(body []byte, session *service.ProxySession, varCtx *VariablesContext, contentType string) []byte { - // only apply rewrite rules for the current host - if hostConfig, ok := session.Config.Load(session.TargetDomain); ok { +func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session *service.ProxySession, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, varCtx *VariablesContext, contentType string) []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 { @@ -2384,13 +2359,23 @@ func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session } } } + + // apply global rewrite rules + 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) + } + } + } + return body } // applyCustomReplacementsWithoutSession applies rewrite rules for requests without session context -func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config map[string]service.ProxyServiceDomainConfig, targetDomain string, contentType string) []byte { - // apply rewrite rules from all host configurations (matches session behavior) - for _, hostConfig := range config { +func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config map[string]service.ProxyServiceDomainConfig, targetDomain string, proxyConfig *service.ProxyServiceConfigYAML, contentType string) []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" { @@ -2400,6 +2385,15 @@ func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config } } + // apply global rewrite rules + 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) + } + } + } + return body } @@ -2887,13 +2881,10 @@ func (m *ProxyHandler) applyEarlyRequestHeaderReplacements(req *http.Request, re applyReplacements(reqCtx.ProxyConfig.Global.Rewrite) } - // then apply request_header replacements from all host configs - // this ensures replacements work for all domains in the session (e.g., CDN domains) + // apply request_header replacements only for the current target domain if reqCtx.ProxyConfig.Hosts != nil { - for _, domainConfig := range reqCtx.ProxyConfig.Hosts { - if domainConfig != nil && domainConfig.Rewrite != nil { - applyReplacements(domainConfig.Rewrite) - } + if domainConfig, exists := reqCtx.ProxyConfig.Hosts[reqCtx.TargetDomain]; exists && domainConfig != nil && domainConfig.Rewrite != nil { + applyReplacements(domainConfig.Rewrite) } } } diff --git a/backend/service/proxy.go b/backend/service/proxy.go index 944cfce..d7fb168 100644 --- a/backend/service/proxy.go +++ b/backend/service/proxy.go @@ -647,12 +647,6 @@ func (m *Proxy) GetByID( return nil, errs.Wrap(err) } - // apply defaults to Proxy configuration for display - if err := m.applyConfigurationDefaults(proxy); err != nil { - m.Logger.Errorw("failed to apply configuration defaults", "error", err) - // don't fail the request, just log the error - } - // no audit log on read return proxy, nil } diff --git a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte index cbf7e5b..12c1764 100644 --- a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte +++ b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte @@ -4,7 +4,7 @@ import TextFieldSelect from '$lib/components/TextFieldSelect.svelte'; import TextareaField from '$lib/components/TextareaField.svelte'; import Search from '$lib/components/Search.svelte'; - import jsyaml from '$lib/components/yaml/index.js'; + import jsyaml, { dumpWithLiteralStrings } from '$lib/components/yaml/index.js'; export let config = null; // basic info fields passed from parent @@ -446,14 +446,8 @@ output[host.domain] = cleanObject(hostObj); } - // serialize to YAML with js-yaml - return jsyaml.dump(output, { - indent: 2, - lineWidth: 120, // allow block scalars for multiline strings - quotingType: "'", // prefer single quotes - forceQuotes: false, // only quote when necessary - noRefs: true // don't use YAML references - }); + // serialize to YAML with literal block style for replace/body fields + return dumpWithLiteralStrings(output); } // host management @@ -1057,14 +1051,8 @@ output[host.domain] = cleanObject(hostObj); } - // serialize to YAML - const yamlContent = jsyaml.dump(output, { - indent: 2, - lineWidth: 120, // allow block scalars for multiline strings - quotingType: "'", - forceQuotes: false, - noRefs: true - }); + // serialize to YAML with literal block style for replace/body fields + const yamlContent = dumpWithLiteralStrings(output); // create blob and download const blob = new Blob([yamlContent], { type: 'application/x-yaml' }); diff --git a/frontend/src/lib/components/yaml/index.js b/frontend/src/lib/components/yaml/index.js index 07874f8..9743d8f 100644 --- a/frontend/src/lib/components/yaml/index.js +++ b/frontend/src/lib/components/yaml/index.js @@ -15,4 +15,93 @@ export const DEFAULT_SCHEMA = jsyaml.DEFAULT_SCHEMA; export const FAILSAFE_SCHEMA = jsyaml.FAILSAFE_SCHEMA; export const JSON_SCHEMA = jsyaml.JSON_SCHEMA; +// helper to dump yaml with literal block style for multiline strings in specific keys +export function dumpWithLiteralStrings(obj, literalKeys = ['replace', 'body'], options = {}) { + // store multiline strings and replace with markers + const multilineStore = new Map(); + let markerIndex = 0; + + function createMarker() { + return `___LITERAL_${markerIndex++}___`; + } + + // recursively process object, replacing multiline strings with markers + function extractMultilineStrings(data, currentKey = null) { + if (Array.isArray(data)) { + return data.map((item) => extractMultilineStrings(item, null)); + } + if (data && typeof data === 'object') { + const result = {}; + for (const [key, value] of Object.entries(data)) { + result[key] = extractMultilineStrings(value, key); + } + return result; + } + // if this is a multiline string in a literal key, replace with marker + if (typeof data === 'string' && literalKeys.includes(currentKey) && data.includes('\n')) { + const marker = createMarker(); + // normalize line endings + const normalized = data.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + multilineStore.set(marker, normalized); + return marker; + } + return data; + } + + // process object to extract multiline strings + const processed = extractMultilineStrings(obj); + + // dump to yaml - markers will be simple strings + let yaml = jsyaml.dump(processed, { + indent: 2, + lineWidth: -1, + noRefs: true, + ...options + }); + + // replace each marker with literal block using simple string search + for (const [marker, value] of multilineStore) { + // find the marker in the yaml (it may be quoted or unquoted) + const patterns = [`'${marker}'`, `"${marker}"`, marker]; + + for (const pattern of patterns) { + const idx = yaml.indexOf(pattern); + if (idx === -1) continue; + + // find the start of this line to get indentation + let lineStart = idx; + while (lineStart > 0 && yaml[lineStart - 1] !== '\n') { + lineStart--; + } + + // extract indentation + const lineContent = yaml.substring(lineStart, idx); + const indentMatch = lineContent.match(/^(\s*)/); + const baseIndent = indentMatch ? indentMatch[1] : ''; + + // find end of line + let lineEnd = idx + pattern.length; + while (lineEnd < yaml.length && yaml[lineEnd] !== '\n') { + lineEnd++; + } + + // get the key from the line (everything before the colon) + const fullLine = yaml.substring(lineStart, lineEnd); + const colonIdx = fullLine.indexOf(':'); + const key = fullLine.substring(baseIndent.length, colonIdx); + + // build literal block + const blockIndent = baseIndent + ' '; + const literalLines = value.split('\n').map((line) => blockIndent + line); + const replacement = `${baseIndent}${key}: |\n${literalLines.join('\n')}`; + + // replace the entire line + yaml = yaml.substring(0, lineStart) + replacement + yaml.substring(lineEnd); + break; + } + } + + return yaml; +} + export default jsyaml; diff --git a/frontend/src/routes/proxy/+page.svelte b/frontend/src/routes/proxy/+page.svelte index f6bc6ab..697d9b8 100644 --- a/frontend/src/routes/proxy/+page.svelte +++ b/frontend/src/routes/proxy/+page.svelte @@ -105,13 +105,8 @@ } // serialize back to YAML without _meta for the config - const cleanYaml = jsyaml.default.dump(parsed, { - indent: 2, - lineWidth: 120, - quotingType: "'", - forceQuotes: false, - noRefs: true - }); + // use dumpWithLiteralStrings to preserve literal block style for replace/body fields + const cleanYaml = jsyaml.dumpWithLiteralStrings(parsed); formValues.proxyConfig = cleanYaml; }); } catch (e) { @@ -121,11 +116,11 @@ // export configuration to YAML file with metadata (for YAML mode) function exportYamlConfig() { - import('$lib/components/yaml/index.js').then((jsyaml) => { + import('$lib/components/yaml/index.js').then((yamlModule) => { try { // parse current config const parsed = formValues.proxyConfig - ? jsyaml.default.load(formValues.proxyConfig) || {} + ? yamlModule.default.load(formValues.proxyConfig) || {} : {}; // build output with _general first @@ -146,14 +141,8 @@ // merge rest of config Object.assign(output, parsed); - // serialize to YAML - const yamlContent = jsyaml.default.dump(output, { - indent: 2, - lineWidth: 120, - quotingType: "'", - forceQuotes: false, - noRefs: true - }); + // serialize to YAML with literal block style for replace/body fields + const yamlContent = yamlModule.dumpWithLiteralStrings(output); // create blob and download const blob = new Blob([yamlContent], { type: 'application/x-yaml' });