<h1><%= @post.title %></h1>
<p><%= @post.author_name %></p>
<%# Only sanitized rich text should be rendered as HTML %>
<div class="prose"><%= sanitize(@post.body_html, tags: %w[p a ul ol li strong em code], attributes: %w[href]) %></div>
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.base_uri :self
policy.object_src :none
policy.frame_ancestors :none
policy.img_src :self, :https, :data
policy.font_src :self, :https, :data
policy.script_src :self
policy.style_src :self, :https, :unsafe_inline
policy.connect_src :self, 'https://api.example.com'
policy.report_uri '/csp-reports'
end
import DOMPurify from 'dompurify';
export function renderPreview(input) {
const sanitized = DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'code', 'pre'],
ALLOWED_ATTR: ['href'],
});
document.querySelector('#preview').innerHTML = sanitized;
}
XSS defense works best in layers: correct output encoding, sanitization for trusted rich text only, and a restrictive Content-Security-Policy. I avoid storing untrusted HTML unless there is a strong product reason. When rich content is required, I sanitize at write time and keep CSP strict enough to limit blast radius.