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
This commit is contained in:
Ronni Skansing
2026-02-05 01:59:43 +01:00
parent 3e3564747b
commit 78b2e57b14
5 changed files with 154 additions and 103 deletions
+54 -63
View File
@@ -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)
}
}
}
-6
View File
@@ -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
}
@@ -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' });
+89
View File
@@ -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;
+6 -17
View File
@@ -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' });