Después de estar un día probando cosas, di con esta respuesta.
Parece que fue un esfuerzo inútil y que debo tomar otro camino.
Cuando asigno a elem.innerHTML <script>alert(1)</script>, se desencripta: (veo que, mientras lo escribo en el editor, si lo escribo sin escapar, se elimina en la vista previa). ¿Es esto un problema o lo detendrá la CSP?
Los mensajes que incluyen etiquetas script como parte de una explicación también parecen generar estos errores de CSP. Ahora estoy realmente confundido. ¿Debo preocuparme por el XSS almacenado o la CSP simplemente lo bloqueará? En el editor uso CKEditor, que previene el self-XSS. Si tengo que preocuparme, parece que necesito eliminar las etiquetas inseguras. Por ahora solo hago:
value = Loofah.fragment(value).scrub!(:escape).to_s
pero parece no tener ningún efecto, ya que asignar este valor a elem.innerHTML simplemente desencripta las entidades HTML.
EDITO: Finalmente encontré la fuente de mi confusión: “Inspeccionar elemento” no muestra el HTML real; ya convierte las entidades HTML.
Si haces clic en “Editar como HTML” en el inspector, queda claro que todo está en realidad bien. Ver etiquetas que no son visibles renderizadas debería haberme indicado esto desde el principio.

