Skip to content

Commit a4a47ff

Browse files
committed
fix(prismic helper) : escape all user-supplied text before HTML interpolation to prevent XSS via CMS content injection : h() on plain text segments and link text, h() on link target attribute, SafeBuffer accumulator in process_text_with_spans, content_tag in build_list_html
1 parent 03e28f1 commit a4a47ff

1 file changed

Lines changed: 53 additions & 68 deletions

File tree

app/helpers/application_helper.rb

Lines changed: 53 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
# frozen_string_literal: true
22

33
module ApplicationHelper
4-
54
def env_class_name
6-
return 'development' if Rails.env.development?
7-
return 'review' if Rails.env.staging? || Rails.env.review?
8-
return 'staging' if request.host.include?('recette')
5+
return "development" if Rails.env.development?
6+
return "review" if Rails.env.staging? || Rails.env.review?
7+
return "staging" if request.host.include?("recette")
98

10-
''
9+
""
1110
end
1211

1312
def helpdesk_url
14-
'https://uneleveunstage.crisp.help/fr/'
13+
"https://uneleveunstage.crisp.help/fr/"
1514
end
1615

1716
# not used
@@ -45,8 +44,8 @@ def account_controller?(user:)
4544

4645
def body_class_name
4746
class_names = []
48-
class_names.push('homepage fr-px-0') if homepage?
49-
class_names.join(' ')
47+
class_names.push("homepage fr-px-0") if homepage?
48+
class_names.join(" ")
5049
end
5150

5251
def homepage?
@@ -58,7 +57,7 @@ def homepage?
5857
# end
5958

6059
def statistics?
61-
controller.class.name.deconstantize == 'Reporting'
60+
controller.class.name.deconstantize == "Reporting"
6261
end
6362

6463
def current_controller?(controller_name)
@@ -69,7 +68,7 @@ def page_title
6968
if content_for?(:page_title)
7069
content_for :page_title
7170
else
72-
default = '1Élève1Stage'
71+
default = "1Élève1Stage"
7372
i18n_key = "#{controller_path.tr('/', '.')}.#{action_name}.page_title"
7473
dyn_page_name = t(i18n_key, default: default)
7574
dyn_page_name == default ? default : "#{dyn_page_name} | #{default}"
@@ -78,30 +77,30 @@ def page_title
7877

7978
def involved_partners_logos
8079
[
81-
{ logo_img: 'airfrance.png', alt: 'airfrance logo' },
82-
{ logo_img: 'bnp.png', alt: 'bnp logo' },
83-
{ logo_img: 'bonduelle.png', alt: 'bonduelle logo' },
84-
{ logo_img: 'bpce.png', alt: 'bpce logo' },
85-
{ logo_img: 'ch-cornouille.png', alt: 'ch-cornouille logo' },
86-
{ logo_img: 'campus-bretagne.png', alt: 'campus bretagne logo' },
87-
{ logo_img: 'ca.png', alt: 'CA logo' },
88-
{ logo_img: 'finances-publiques.png', alt: 'finances publiques logo' },
89-
{ logo_img: 'gendarmerie.png', alt: 'gendarmerie logo' },
90-
{ logo_img: 'laposte.png', alt: 'laposte logo' },
91-
{ logo_img: 'min-interieur.png', alt: 'min interieur logo' },
92-
{ logo_img: 'normandie-manutention.png', alt: 'normandie manutention logo' },
93-
{ logo_img: 'orchestre.png', alt: 'orchestre national logo' },
94-
{ logo_img: 'orchestre-euro.png', alt: 'orchestre europeen logo' },
95-
{ logo_img: 'police.png', alt: 'police logo' },
96-
{ logo_img: 'renault.png', alt: 'renault logo' },
97-
{ logo_img: 'rte.png', alt: 'rte logo' },
98-
{ logo_img: 'safran.png', alt: 'safran logo' },
99-
{ logo_img: 'saint-gobain.png', alt: 'saint gobain logo' },
100-
{ logo_img: 'sogetrel.png', alt: 'sogetrel logo' },
101-
{ logo_img: 'suez.png', alt: 'suez logo' },
102-
{ logo_img: 'thales.png', alt: 'thales logo' },
103-
{ logo_img: 'mairie-toulouse.png', alt: 'mairie toulouse logo' },
104-
{ logo_img: 'univ-rennes.png', alt: 'univ rennes logo' }
80+
{ logo_img: "airfrance.png", alt: "airfrance logo" },
81+
{ logo_img: "bnp.png", alt: "bnp logo" },
82+
{ logo_img: "bonduelle.png", alt: "bonduelle logo" },
83+
{ logo_img: "bpce.png", alt: "bpce logo" },
84+
{ logo_img: "ch-cornouille.png", alt: "ch-cornouille logo" },
85+
{ logo_img: "campus-bretagne.png", alt: "campus bretagne logo" },
86+
{ logo_img: "ca.png", alt: "CA logo" },
87+
{ logo_img: "finances-publiques.png", alt: "finances publiques logo" },
88+
{ logo_img: "gendarmerie.png", alt: "gendarmerie logo" },
89+
{ logo_img: "laposte.png", alt: "laposte logo" },
90+
{ logo_img: "min-interieur.png", alt: "min interieur logo" },
91+
{ logo_img: "normandie-manutention.png", alt: "normandie manutention logo" },
92+
{ logo_img: "orchestre.png", alt: "orchestre national logo" },
93+
{ logo_img: "orchestre-euro.png", alt: "orchestre europeen logo" },
94+
{ logo_img: "police.png", alt: "police logo" },
95+
{ logo_img: "renault.png", alt: "renault logo" },
96+
{ logo_img: "rte.png", alt: "rte logo" },
97+
{ logo_img: "safran.png", alt: "safran logo" },
98+
{ logo_img: "saint-gobain.png", alt: "saint gobain logo" },
99+
{ logo_img: "sogetrel.png", alt: "sogetrel logo" },
100+
{ logo_img: "suez.png", alt: "suez logo" },
101+
{ logo_img: "thales.png", alt: "thales logo" },
102+
{ logo_img: "mairie-toulouse.png", alt: "mairie toulouse logo" },
103+
{ logo_img: "univ-rennes.png", alt: "univ rennes logo" }
105104
]
106105
end
107106

@@ -111,13 +110,13 @@ def generate_breadcrumb_links(*links)
111110
if link.is_a?(Array)
112111
{ path: link[0], name: link[1] }
113112
else
114-
{ path: '', name: link }
113+
{ path: "", name: link }
115114
end
116115
end
117116
end
118117

119118
def prismic_structured_text_to_html(prismic_fragment)
120-
return '' if prismic_fragment.blank? || prismic_fragment.blocks.blank?
119+
return "" if prismic_fragment.blank? || prismic_fragment.blocks.blank?
121120

122121
html_parts = []
123122
current_list_items = []
@@ -156,44 +155,35 @@ def prismic_structured_text_to_html(prismic_fragment)
156155
# Fermer la dernière liste si elle existe
157156
html_parts << build_list_html(current_list_items, current_list_ordered) if current_list_items.any?
158157

159-
html_parts.join("\n").html_safe
158+
html_parts.safe_join("\n")
160159
end
161160

162161
def js_email_pattern = '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$'
163162

164163
private
165164

166165
def process_text_with_spans(text, spans)
167-
return text if spans.blank?
166+
return h(text) if spans.blank?
168167

169-
# Trier les spans par position de début
170-
sorted_spans = spans.sort_by(&:start)
171-
172-
# Construire le HTML avec les liens
173-
result = ''
168+
result = "".html_safe
174169
last_end = 0
175170

176-
sorted_spans.each do |span|
177-
# Ajouter le texte avant le span
178-
result += text[last_end...span.start] if span.start > last_end
171+
spans.sort_by(&:start).each do |span|
172+
result << h(text[last_end...span.start]) if span.start > last_end
179173

180-
# Traiter le span selon son type
181174
case span
182175
when Prismic::Fragments::StructuredText::Span::Hyperlink
183-
link_text = text[span.start...span.end]
176+
link_text = h(text[span.start...span.end])
184177
link_attributes = build_link_attributes(span.link)
185-
result += "<a #{link_attributes}>#{link_text}</a>"
178+
result << "<a #{link_attributes}>#{link_text}</a>".html_safe
186179
else
187-
# Pour les autres types de spans, ajouter le texte tel quel
188-
result += text[span.start...span.end]
180+
result << h(text[span.start...span.end])
189181
end
190182

191183
last_end = span.end
192184
end
193185

194-
# Ajouter le texte restant après le dernier span
195-
result += text[last_end..-1] if last_end < text.length
196-
186+
result << h(text[last_end..]) if last_end < text.length
197187
result
198188
end
199189

@@ -202,26 +192,21 @@ def build_link_attributes(link)
202192

203193
case link
204194
when Prismic::Fragments::WebLink
205-
# S'assurer que l'URL est correctement échappée
206-
safe_url = h(link.url.to_s)
207-
attributes << "href=\"#{safe_url}\""
208-
attributes << "target=\"#{link.target}\"" if link.target.present?
209-
attributes << 'rel="noopener noreferrer"' if link.target == '_blank'
195+
attributes << "href=\"#{h(link.url.to_s)}\""
196+
attributes << "target=\"#{h(link.target)}\"" if link.target.present?
197+
attributes << 'rel="noopener noreferrer"' if link.target == "_blank"
210198
when Prismic::Fragments::DocumentLink
211-
# Pour les liens internes vers d'autres documents Prismic
212-
safe_url = h(link.url.to_s)
213-
attributes << "href=\"#{safe_url}\""
199+
attributes << "href=\"#{h(link.url.to_s)}\""
214200
end
215201

216-
attributes.join(' ')
202+
attributes.join(" ")
217203
end
218204

219205
def build_list_html(list_items, ordered)
220-
return '' if list_items.empty?
221-
222-
tag = ordered ? 'ol' : 'ul'
223-
items_html = list_items.map { |item| "<li>#{item}</li>" }.join("\n")
206+
return "".html_safe if list_items.empty?
224207

225-
"<#{tag}>\n#{items_html}\n</#{tag}>"
208+
tag = ordered ? "ol" : "ul"
209+
items_html = safe_join(list_items.map { |item| content_tag(:li, item) }, "\n")
210+
content_tag(tag, "\n#{items_html}\n".html_safe)
226211
end
227212
end

0 commit comments

Comments
 (0)