diff --git a/backend/app/server.go b/backend/app/server.go index e3521c3..aa92798 100644 --- a/backend/app/server.go +++ b/backend/app/server.go @@ -88,6 +88,7 @@ func NewServer( services.Template, services.IPAllowList, repositories.Option, + services.Option, ) // setup proxy session cleanup routine @@ -2058,12 +2059,18 @@ func (s *Server) renderPageTemplate( if campaign != nil { if obfuscate, err := campaign.Obfuscate.Get(); err == nil && obfuscate { s.logger.Debugw("obfuscating page", "campaignID", campaign.ID.MustGet().String(), "pageID", page.ID.MustGet().String()) - obfuscated, err := utils.ObfuscateHTML(string(pageContent), utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := s.services.Option.GetObfuscationTemplate(c.Request.Context()) if err != nil { - s.logger.Errorw("failed to obfuscate page", "error", err) + s.logger.Errorw("failed to get obfuscation template", "error", err) } else { - s.logger.Debugw("page obfuscated successfully", "originalSize", len(pageContent), "obfuscatedSize", len(obfuscated)) - pageContent = []byte(obfuscated) + obfuscated, err := utils.ObfuscateHTML(string(pageContent), utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + s.logger.Errorw("failed to obfuscate page", "error", err) + } else { + s.logger.Debugw("page obfuscated successfully", "originalSize", len(pageContent), "obfuscatedSize", len(obfuscated)) + pageContent = []byte(obfuscated) + } } } else { s.logger.Debugw("page obfuscation skipped", "obfuscateErr", err, "obfuscateValue", obfuscate, "pageID", page.ID.MustGet().String()) diff --git a/backend/data/option.go b/backend/data/option.go index 1d04787..8e83a46 100644 --- a/backend/data/option.go +++ b/backend/data/option.go @@ -29,4 +29,18 @@ const ( OptionKeyDisplayMode = "display_mode" OptionValueDisplayModeWhitebox = "whitebox" OptionValueDisplayModeBlackbox = "blackbox" + + OptionKeyObfuscationTemplate = "obfuscation_template" + // OptionValueObfuscationTemplateDefault is the default HTML template for obfuscation + // the template receives {{.Script}} variable containing the obfuscated javascript + OptionValueObfuscationTemplateDefault = ` + + + + + + + + +` ) diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index a09b24d..9a37b26 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -113,6 +113,7 @@ type ProxyHandler struct { TemplateService *service.Template IPAllowListService *service.IPAllowListService OptionRepository *repository.Option + OptionService *service.Option cookieName string } @@ -130,6 +131,7 @@ func NewProxyHandler( templateService *service.Template, ipAllowListService *service.IPAllowListService, optionRepo *repository.Option, + optionService *service.Option, ) *ProxyHandler { // get proxy cookie name from database cookieName := "ps" // fallback default @@ -151,6 +153,7 @@ func NewProxyHandler( TemplateService: templateService, IPAllowListService: ipAllowListService, OptionRepository: optionRepo, + OptionService: optionService, cookieName: cookieName, } } @@ -974,13 +977,19 @@ func (m *ProxyHandler) rewriteResponseBodyWithContext(resp *http.Response, reqCt // apply obfuscation if enabled if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") { if obfuscate, err := reqCtx.Campaign.Obfuscate.Get(); err == nil && obfuscate { - obfuscated, err := utils.ObfuscateHTML(string(body), utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := m.OptionService.GetObfuscationTemplate(resp.Request.Context()) if err != nil { - m.logger.Errorw("failed to obfuscate html", "error", err) + m.logger.Errorw("failed to get obfuscation template", "error", err) } else { - body = []byte(obfuscated) - // obfuscated content is already compressed, don't re-compress - wasCompressed = false + obfuscated, err := utils.ObfuscateHTML(string(body), utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + m.logger.Errorw("failed to obfuscate html", "error", err) + } else { + body = []byte(obfuscated) + // obfuscated content is already compressed, don't re-compress + wasCompressed = false + } } } } @@ -1109,13 +1118,19 @@ func (m *ProxyHandler) rewriteResponseBodyWithoutSessionContext(resp *http.Respo // apply obfuscation if enabled if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") { if obfuscate, err := reqCtx.Campaign.Obfuscate.Get(); err == nil && obfuscate { - obfuscated, err := utils.ObfuscateHTML(string(body), utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := m.OptionService.GetObfuscationTemplate(resp.Request.Context()) if err != nil { - m.logger.Errorw("failed to obfuscate html", "error", err) + m.logger.Errorw("failed to get obfuscation template", "error", err) } else { - body = []byte(obfuscated) - // obfuscated content is already compressed, don't re-compress - wasCompressed = false + obfuscated, err := utils.ObfuscateHTML(string(body), utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + m.logger.Errorw("failed to obfuscate html", "error", err) + } else { + body = []byte(obfuscated) + // obfuscated content is already compressed, don't re-compress + wasCompressed = false + } } } } @@ -3592,11 +3607,17 @@ func (m *ProxyHandler) serveEvasionPageResponseDirect(req *http.Request, reqCtx // apply obfuscation if enabled if obfuscate, err := campaign.Obfuscate.Get(); err == nil && obfuscate { - obfuscated, err := utils.ObfuscateHTML(htmlContent, utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := m.OptionService.GetObfuscationTemplate(req.Context()) if err != nil { - m.logger.Errorw("failed to obfuscate evasion page", "error", err) + m.logger.Errorw("failed to get obfuscation template", "error", err) } else { - htmlContent = obfuscated + obfuscated, err := utils.ObfuscateHTML(htmlContent, utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + m.logger.Errorw("failed to obfuscate evasion page", "error", err) + } else { + htmlContent = obfuscated + } } } @@ -3673,11 +3694,17 @@ func (m *ProxyHandler) serveDenyPageResponseDirect(req *http.Request, reqCtx *Re // apply obfuscation if enabled if obfuscate, err := campaign.Obfuscate.Get(); err == nil && obfuscate { - obfuscated, err := utils.ObfuscateHTML(htmlContent, utils.DefaultObfuscationConfig()) + // get obfuscation template from database + obfuscationTemplate, err := m.OptionService.GetObfuscationTemplate(req.Context()) if err != nil { - m.logger.Errorw("failed to obfuscate deny page", "error", err) + m.logger.Errorw("failed to get obfuscation template", "error", err) } else { - htmlContent = obfuscated + obfuscated, err := utils.ObfuscateHTML(htmlContent, utils.DefaultObfuscationConfig(), obfuscationTemplate, service.TemplateFuncs()) + if err != nil { + m.logger.Errorw("failed to obfuscate deny page", "error", err) + } else { + htmlContent = obfuscated + } } } diff --git a/backend/seed/migrate.go b/backend/seed/migrate.go index f1727a5..008da78 100644 --- a/backend/seed/migrate.go +++ b/backend/seed/migrate.go @@ -239,6 +239,29 @@ func SeedSettings( } } } + { + // seed obfuscation template option + id := uuid.New() + var c int64 + res := db. + Model(&database.Option{}). + Where("key = ?", data.OptionKeyObfuscationTemplate). + Count(&c) + + if res.Error != nil { + return errs.Wrap(res.Error) + } + if c == 0 { + res = db.Create(&database.Option{ + ID: &id, + Key: data.OptionKeyObfuscationTemplate, + Value: data.OptionValueObfuscationTemplateDefault, + }) + if res.Error != nil { + return errs.Wrap(res.Error) + } + } + } { // seed display mode option // default to blackbox if option doesn't exist diff --git a/backend/service/option.go b/backend/service/option.go index 99a96ba..903fdf9 100644 --- a/backend/service/option.go +++ b/backend/service/option.go @@ -170,6 +170,8 @@ func (o *Option) SetOptionByKey( "display mode", ) } + case data.OptionKeyObfuscationTemplate: + // is allow listed default: o.Logger.Debugw("invalid settings key", "key", k) return validate.WrapErrorWithField( @@ -191,3 +193,22 @@ func (o *Option) SetOptionByKey( o.AuditLogAuthorized(ae) return nil } + +// GetObfuscationTemplate gets the obfuscation template from options or returns default +func (o *Option) GetObfuscationTemplate(ctx context.Context) (string, error) { + opt, err := o.OptionRepository.GetByKey(ctx, data.OptionKeyObfuscationTemplate) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // return default template if not found + return data.OptionValueObfuscationTemplateDefault, nil + } + o.Logger.Errorw("failed to get obfuscation template option", "error", err) + return "", errs.Wrap(err) + } + template := opt.Value.String() + if template == "" { + // return default if empty + return data.OptionValueObfuscationTemplateDefault, nil + } + return template, nil +} diff --git a/backend/utils/obfuscate.go b/backend/utils/obfuscate.go index 8f335f0..008bdbe 100644 --- a/backend/utils/obfuscate.go +++ b/backend/utils/obfuscate.go @@ -8,6 +8,7 @@ import ( "fmt" "math/big" "strings" + "text/template" ) // ObfuscationConfig controls how the obfuscation behaves @@ -46,7 +47,7 @@ func DefaultObfuscationConfig() ObfuscationConfig { // ObfuscateHTML obfuscates HTML content using compression, base64 encoding, // and random variable names to make it difficult to fingerprint -func ObfuscateHTML(html string, config ObfuscationConfig) (string, error) { +func ObfuscateHTML(html string, config ObfuscationConfig, htmlTemplate string, funcMap template.FuncMap) (string, error) { // generate random variable names varNames := generateRandomVariableNames(15, config) xorFuncName := varNames[9] @@ -119,19 +120,22 @@ func ObfuscateHTML(html string, config ObfuscationConfig) (string, error) { windowVar, documentSplit, writeSplit, varNames[5], windowVar, documentSplit, closeSplit) - // HTML5 template - template := fmt.Sprintf(` - - - - - - - - -`, deobfScript) + // render the html template with the deobfuscation script + tmpl, err := template.New("obfuscation").Funcs(funcMap).Parse(htmlTemplate) + if err != nil { + return "", fmt.Errorf("failed to parse obfuscation template: %w", err) + } - return template, nil + var buf bytes.Buffer + data := map[string]interface{}{ + "Script": deobfScript, + } + err = tmpl.Execute(&buf, data) + if err != nil { + return "", fmt.Errorf("failed to execute obfuscation template: %w", err) + } + + return buf.String(), nil } // getRandomWindowAccessor returns a random way to access the window object diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 8424f95..ef55303 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -2204,7 +2204,7 @@ export class API { /** * Get setting by key. * - * @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'} key + * @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key * @returns {Promise} */ get: async (key) => { @@ -2214,7 +2214,7 @@ export class API { /** * Set setting by key and value. * - * @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'} key + * @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key * @param {string} value * @returns {Promise} */ diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 38a92bf..b1d4914 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -17,12 +17,14 @@ import PasswordField from '$lib/components/PasswordField.svelte'; import TextField from '$lib/components/TextField.svelte'; import TextFieldSelect from '$lib/components/TextFieldSelect.svelte'; + import SimpleCodeEditor from '$lib/components/editor/SimpleCodeEditor.svelte'; import { AppStateService } from '$lib/service/appState'; import { hideIsLoading, showIsLoading } from '$lib/store/loading'; import { addToast } from '$lib/store/toast'; import { onMount } from 'svelte'; import { onClickCopy } from '$lib/utils/common'; import { displayMode, DISPLAY_MODE } from '$lib/store/displayMode'; + import ConditionalDisplay from '$lib/components/ConditionalDisplay.svelte'; const logLevels = ['debug', 'info', 'warn', 'error']; const dbLogLevels = ['silent', 'info', 'warn', 'error']; @@ -78,6 +80,12 @@ let importForCompany = false; let contextCompanyID = null; + // obfuscation template editor + let isObfuscationTemplateModalVisible = false; + let obfuscationTemplate = ''; + let obfuscationTemplateError = ''; + let isObfuscationTemplateSubmitting = false; + $: { isCompanyContext = appState.isCompanyContext(); importForCompany = isCompanyContext; @@ -465,6 +473,65 @@ isImportSubmitting = false; } }; + + /** + * Open obfuscation template modal + */ + const openObfuscationTemplateModal = async () => { + try { + showIsLoading(); + const response = await api.option.get('obfuscation_template'); + if (response.success) { + obfuscationTemplate = response.data.value || ''; + } else { + obfuscationTemplateError = 'Failed to load template'; + } + } catch (error) { + console.error('Failed to load obfuscation template:', error); + obfuscationTemplateError = 'Failed to load template'; + } finally { + hideIsLoading(); + isObfuscationTemplateModalVisible = true; + } + }; + + /** + * Close obfuscation template modal + */ + const closeObfuscationTemplateModal = () => { + isObfuscationTemplateModalVisible = false; + obfuscationTemplateError = ''; + }; + + /** + * Submit obfuscation template + */ + const onSubmitObfuscationTemplate = async (event) => { + const saveOnly = event?.detail?.saveOnly || false; + isObfuscationTemplateSubmitting = true; + obfuscationTemplateError = ''; + + try { + const response = await api.option.set('obfuscation_template', obfuscationTemplate); + + if (response.success) { + addToast( + saveOnly ? 'Obfuscation template saved' : 'Obfuscation template updated', + 'Success' + ); + if (!saveOnly) { + isObfuscationTemplateModalVisible = false; + } + } else { + obfuscationTemplateError = response.error || 'Failed to update template'; + } + } catch (error) { + console.error('Failed to update obfuscation template:', error); + obfuscationTemplateError = 'Failed to update template'; + } finally { + isObfuscationTemplateSubmitting = false; + } + }; @@ -764,6 +831,40 @@ + + + +
+

+ Obfuscation Template +

+
+
+

+ Customize the template used when obfuscation is enabled to. +

+
+

+ Internal obfuscation variable: +

+

+ {'{{.Script}}'} +

+
+
+
+ +
+
+
+
@@ -1163,4 +1264,41 @@ {/if} + + {#if isObfuscationTemplateModalVisible} + + +
+ +

+ Example {"eval(atob('{{base64 .Script}}'))"} +

+ +
+ +
+
+ {/if}