fix(tags): escape user data tags for emails

This commit is contained in:
Colin Darie 2023-12-15 12:40:50 +01:00
parent 7ba13507ff
commit fa37c6c62b
No known key found for this signature in database
GPG key ID: 8C76CADD40253590
4 changed files with 43 additions and 9 deletions

View file

@ -18,7 +18,7 @@ class AttestationTemplate < ApplicationRecord
DOSSIER_STATE = Dossier.states.fetch(:accepte) DOSSIER_STATE = Dossier.states.fetch(:accepte)
def attestation_for(dossier) def attestation_for(dossier)
attestation = Attestation.new(title: replace_tags(title, dossier)) attestation = Attestation.new(title: replace_tags(title, dossier, escape: false))
attestation.pdf.attach( attestation.pdf.attach(
io: build_pdf(dossier), io: build_pdf(dossier),
filename: "attestation-dossier-#{dossier.id}.pdf", filename: "attestation-dossier-#{dossier.id}.pdf",
@ -70,8 +70,8 @@ class AttestationTemplate < ApplicationRecord
if dossier.present? if dossier.present?
attributes.merge({ attributes.merge({
title: replace_tags(title, dossier), title: replace_tags(title, dossier, escape: false),
body: replace_tags(body, dossier), body: replace_tags(body, dossier, escape: false),
signature: signature_to_render(dossier.groupe_instructeur) signature: signature_to_render(dossier.groupe_instructeur)
}) })
else else

View file

@ -66,6 +66,7 @@ module TagsSubstitutionConcern
libelle: 'motivation', libelle: 'motivation',
description: 'Motivation facultative associée à la décision finale dacceptation, refus ou classement sans suite', description: 'Motivation facultative associée à la décision finale dacceptation, refus ou classement sans suite',
lambda: -> (d) { simple_format(d.motivation) }, lambda: -> (d) { simple_format(d.motivation) },
escapable: false, # sanitized by simple_format
available_for_states: Dossier::TERMINE available_for_states: Dossier::TERMINE
}, },
{ {
@ -118,14 +119,16 @@ module TagsSubstitutionConcern
libelle: 'lien dossier', libelle: 'lien dossier',
description: '', description: '',
lambda: -> (d) { external_link(dossier_url(d)) }, lambda: -> (d) { external_link(dossier_url(d)) },
available_for_states: Dossier::SOUMIS available_for_states: Dossier::SOUMIS,
escapable: false
}, },
{ {
id: 'dossier_attestation_url', id: 'dossier_attestation_url',
libelle: 'lien attestation', libelle: 'lien attestation',
description: '', description: '',
lambda: -> (d) { external_link(attestation_dossier_url(d)) }, lambda: -> (d) { external_link(attestation_dossier_url(d)) },
available_for_states: [Dossier.states.fetch(:accepte)] available_for_states: [Dossier.states.fetch(:accepte)],
escapable: false
}, },
{ {
id: 'dossier_motivation_url', id: 'dossier_motivation_url',
@ -138,7 +141,8 @@ module TagsSubstitutionConcern
return "[linstructeur na pas joint de document supplémentaire]" return "[linstructeur na pas joint de document supplémentaire]"
end end
}, },
available_for_states: Dossier::TERMINE available_for_states: Dossier::TERMINE,
escapable: false
} }
] ]
@ -310,11 +314,13 @@ module TagsSubstitutionConcern
tags tags
end end
def replace_tags(text, dossier) def replace_tags(text, dossier, escape: true)
if text.nil? if text.nil?
return '' return ''
end end
@escape_unsafe_tags = escape
tokens = parse_tags(text) tokens = parse_tags(text)
tags_and_datas = [ tags_and_datas = [
@ -352,11 +358,21 @@ module TagsSubstitutionConcern
end end
def replace_tag(tag, data) def replace_tag(tag, data)
if tag.key?(:target) value = if tag.key?(:target)
data.public_send(tag[:target]) data.public_send(tag[:target])
else else
instance_exec(data, &tag[:lambda]) instance_exec(data, &tag[:lambda])
end end
if escape_unsafe_tags? && tag.fetch(:escapable, true)
escape_once(value)
else
value
end
end
def escape_unsafe_tags?
@escape_unsafe_tags
end end
def procedure_types_de_champ_tags def procedure_types_de_champ_tags

View file

@ -130,7 +130,7 @@ RSpec.describe NotificationMailer, type: :mailer do
context "subject has a special character" do context "subject has a special character" do
let(:subject) { '--libellé démarche--' } let(:subject) { '--libellé démarche--' }
it { expect(mail.subject).to eq("Mon titre avec l'apostrophe") } it { expect(mail.subject).to eq("Mon titre avec l&#39;apostrophe") }
end end
end end
end end

View file

@ -411,6 +411,24 @@ describe TagsSubstitutionConcern, type: :model do
end end
end end
end end
context 'when data contains malicious code' do
let(:template) { '--libelleA-- --nom--' }
context 'in individual data' do
let(:for_individual) { true }
let(:individual) { create(:individual, nom: '<a href="https://oops.com">name</a>') }
it { is_expected.to eq('--libelleA-- &lt;a href=&quot;https://oops.com&quot;&gt;name&lt;/a&gt;') }
end
context 'in a champ' do
let(:types_de_champ_public) { [{ libelle: 'libelleA' }] }
before { dossier.champs_public.first.update(value: 'hey <a href="https://oops.com">anchor</a>') }
it { is_expected.to eq('hey &lt;a href=&quot;https://oops.com&quot;&gt;anchor&lt;/a&gt; --nom--') }
end
end
end end
describe 'tags' do describe 'tags' do