From c1709c24aa85f3f14d93e73f46495fbc783f95e3 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Tue, 16 Sep 2025 16:42:40 +0200 Subject: [PATCH] perform validation on save Signed-off-by: Ronni Skansing --- backend/app/services.go | 2 + backend/service/domain.go | 24 ++++++++ backend/service/email.go | 12 ++++ backend/service/page.go | 13 ++++ backend/service/templateService.go | 99 ++++++++++++++++++++++++++++++ 5 files changed, 150 insertions(+) diff --git a/backend/app/services.go b/backend/app/services.go index fb23e4a..761aab8 100644 --- a/backend/app/services.go +++ b/backend/app/services.go @@ -138,6 +138,7 @@ func NewServices( CampaignRepository: repositories.Campaign, PageRepository: repositories.Page, CampaignTemplateService: campaignTemplate, + TemplateService: templateService, } domain := &service.Domain{ Common: common, @@ -149,6 +150,7 @@ func NewServices( CampaignTemplateService: campaignTemplate, AssetService: asset, FileService: file, + TemplateService: templateService, } email := &service.Email{ Common: common, diff --git a/backend/service/domain.go b/backend/service/domain.go index 6954fc1..06c1891 100644 --- a/backend/service/domain.go +++ b/backend/service/domain.go @@ -36,6 +36,7 @@ type Domain struct { CampaignTemplateService *CampaignTemplate AssetService *Asset FileService *File + TemplateService *Template } // Create creates a new domain @@ -60,6 +61,19 @@ func (d *Domain) Create( // d.Logger.Debugf("failed to validate domain", "error", err) return nil, errs.Wrap(err) } + // validate template content if present + if pageContent, err := domain.PageContent.Get(); err == nil { + if err := d.TemplateService.ValidateDomainTemplate(pageContent.String()); err != nil { + d.Logger.Errorw("failed to validate domain page template", "error", err) + return nil, validate.WrapErrorWithField(errors.New("invalid page template: "+err.Error()), "pageContent") + } + } + if notFoundContent, err := domain.PageNotFoundContent.Get(); err == nil { + if err := d.TemplateService.ValidateDomainTemplate(notFoundContent.String()); err != nil { + d.Logger.Errorw("failed to validate domain not found template", "error", err) + return nil, validate.WrapErrorWithField(errors.New("invalid not found template: "+err.Error()), "pageNotFoundContent") + } + } // check for uniqueness name := domain.Name.MustGet() // safe as we have validated _, err = d.DomainRepository.GetByName( @@ -403,9 +417,19 @@ func (d *Domain) UpdateByID( current.HostWebsite.Set(v) } if v, err := incoming.PageContent.Get(); err == nil { + // validate template content before updating + if err := d.TemplateService.ValidateDomainTemplate(v.String()); err != nil { + d.Logger.Errorw("failed to validate domain page template", "error", err) + return validate.WrapErrorWithField(errors.New("invalid page template: "+err.Error()), "pageContent") + } current.PageContent.Set(v) } if v, err := incoming.PageNotFoundContent.Get(); err == nil { + // validate template content before updating + if err := d.TemplateService.ValidateDomainTemplate(v.String()); err != nil { + d.Logger.Errorw("failed to validate domain not found template", "error", err) + return validate.WrapErrorWithField(errors.New("invalid not found template: "+err.Error()), "pageNotFoundContent") + } current.PageNotFoundContent.Set(v) } if v, err := incoming.RedirectURL.Get(); err == nil { diff --git a/backend/service/email.go b/backend/service/email.go index a2139d8..77bb5b1 100644 --- a/backend/service/email.go +++ b/backend/service/email.go @@ -171,6 +171,13 @@ func (m *Email) Create( if err := email.Validate(); err != nil { return nil, errs.Wrap(err) } + // validate template content if present + if content, err := email.Content.Get(); err == nil { + if err := m.TemplateService.ValidateEmailTemplate(content.String()); err != nil { + m.Logger.Errorw("failed to validate email template", "error", err) + return nil, validate.WrapErrorWithField(errors.New("invalid template: "+err.Error()), "content") + } + } // check uniqueness var companyID *uuid.UUID if cid, err := email.CompanyID.Get(); err == nil { @@ -780,6 +787,11 @@ func (m *Email) UpdateByID( current.MailHeaderSubject.Set(v) } if v, err := email.Content.Get(); err == nil { + // validate template content before updating + if err := m.TemplateService.ValidateEmailTemplate(v.String()); err != nil { + m.Logger.Errorw("failed to validate email template", "error", err) + return validate.WrapErrorWithField(errors.New("invalid template: "+err.Error()), "content") + } if _, err := email.AddTrackingPixel.Get(); err == nil { // handle tracking pixel email, err = m.toggleTrackingPixel(email) diff --git a/backend/service/page.go b/backend/service/page.go index 2fe46d2..ff7b98c 100644 --- a/backend/service/page.go +++ b/backend/service/page.go @@ -20,6 +20,7 @@ type Page struct { PageRepository *repository.Page CampaignRepository *repository.Campaign CampaignTemplateService *CampaignTemplate + TemplateService *Template } // Create creates a new page @@ -49,6 +50,13 @@ func (p *Page) Create( p.Logger.Errorw("failed to validate page", "error", err) return nil, errs.Wrap(err) } + // validate template content if present + if content, err := page.Content.Get(); err == nil { + if err := p.TemplateService.ValidatePageTemplate(content.String()); err != nil { + p.Logger.Errorw("failed to validate page template", "error", err) + return nil, validate.WrapErrorWithField(errors.New("invalid template: "+err.Error()), "content") + } + } // check uniqueness name := page.Name.MustGet() isOK, err := repository.CheckNameIsUnique( @@ -253,6 +261,11 @@ func (p *Page) UpdateByID( current.Name.Set(v) } if v, err := page.Content.Get(); err == nil { + // validate template content before updating + if err := p.TemplateService.ValidatePageTemplate(v.String()); err != nil { + p.Logger.Errorw("failed to validate page template", "error", err) + return validate.WrapErrorWithField(errors.New("invalid template: "+err.Error()), "content") + } current.Content.Set(v) } // update page diff --git a/backend/service/templateService.go b/backend/service/templateService.go index 8514b76..ecde82d 100644 --- a/backend/service/templateService.go +++ b/backend/service/templateService.go @@ -18,6 +18,7 @@ import ( "github.com/phishingclub/phishingclub/errs" "github.com/phishingclub/phishingclub/model" "github.com/phishingclub/phishingclub/utils" + "github.com/phishingclub/phishingclub/vo" "github.com/yeqown/go-qrcode/v2" ) @@ -71,6 +72,104 @@ func (t *Template) CreateMail( ) } +// ValidatePageTemplate validates that a page template can be parsed and executed without errors +func (t *Template) ValidatePageTemplate(content string) error { + // use the same parsing approach as CreatePhishingPage but without executing + _, err := template.New("validation"). + Funcs(TemplateFuncs()). + Parse(content) + + if err != nil { + return fmt.Errorf("failed to parse page template: %s", err) + } + + // also try to execute with mock data to catch runtime errors + _, err = t.ApplyPageMock(content) + if err != nil { + return fmt.Errorf("failed to execute page template: %s", err) + } + + return nil +} + +// ValidateEmailTemplate validates that an email template can be parsed and executed without errors +func (t *Template) ValidateEmailTemplate(content string) error { + // use the same parsing approach as email creation but without executing + _, err := template.New("validation"). + Funcs(TemplateFuncs()). + Parse(content) + + if err != nil { + return fmt.Errorf("failed to parse email template: %s", err) + } + + // also try to execute with mock data to catch runtime errors + domain := &model.Domain{ + Name: nullable.NewNullableWithValue( + *vo.NewString255Must("example.test"), + ), + } + recipient := model.NewRecipientExample() + campaignRecipient := model.CampaignRecipient{ + ID: nullable.NewNullableWithValue( + uuid.New(), + ), + Recipient: recipient, + } + email := model.NewEmailExample() + email.Content = nullable.NewNullableWithValue( + *vo.NewUnsafeOptionalString1MB(content), + ) + apiSender := model.NewAPISenderExample() + + _, err = t.CreateMailBody( + "id", + "/test", + domain, + &campaignRecipient, + email, + apiSender, + ) + if err != nil { + return fmt.Errorf("failed to execute email template: %s", err) + } + + return nil +} + +// ValidateDomainTemplate validates that a domain template can be parsed and executed without errors +func (t *Template) ValidateDomainTemplate(content string) error { + // use the same parsing approach as domain content but without executing + _, err := template.New("validation"). + Funcs(TemplateFuncs()). + Parse(content) + + if err != nil { + return fmt.Errorf("failed to parse domain template: %s", err) + } + + // also try to execute with mock data to catch runtime errors + // domains only have access to BaseURL variable + data := map[string]any{ + "BaseURL": "https://example.test", + } + + tmpl, err := template.New("domain"). + Funcs(TemplateFuncs()). + Parse(content) + if err != nil { + return fmt.Errorf("failed to parse domain template: %s", err) + } + + var buf bytes.Buffer + err = tmpl.Execute(&buf, data) + if err != nil { + return fmt.Errorf("failed to execute domain template: %s", err) + } + + return nil +} + // ApplyPageMock func (t *Template) ApplyPageMock(content string) (*bytes.Buffer, error) { // build response