mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 09:46:22 +00:00
sanitizar entradas antes de guardarlas para prevenir xss #75
This commit is contained in:
parent
2edcf58d64
commit
8a9197c390
18 changed files with 90 additions and 45 deletions
|
@ -10,4 +10,16 @@ class MetadataArray < MetadataTemplate
|
||||||
def to_param
|
def to_param
|
||||||
{ name => [] }
|
{ name => [] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sanitize(values)
|
||||||
|
values.map do |v|
|
||||||
|
if v.is_a? String
|
||||||
|
super(v)
|
||||||
|
else
|
||||||
|
v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Un campo de correo
|
# Un campo de color
|
||||||
class MetadataColor < MetadataString; end
|
class MetadataColor < MetadataString; end
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
# Se encarga del contenido del artículo y quizás otros campos que
|
# Se encarga del contenido del artículo y quizás otros campos que
|
||||||
# requieran texto largo.
|
# requieran texto largo.
|
||||||
class MetadataContent < MetadataTemplate
|
class MetadataContent < MetadataTemplate
|
||||||
include ActionView::Helpers::SanitizeHelper
|
|
||||||
|
|
||||||
def default_value
|
def default_value
|
||||||
''
|
''
|
||||||
end
|
end
|
||||||
|
@ -16,22 +14,4 @@ class MetadataContent < MetadataTemplate
|
||||||
def front_matter?
|
def front_matter?
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Etiquetas y atributos HTML a permitir
|
|
||||||
#
|
|
||||||
# No queremos permitir mucho más que cosas de las que nos falten en
|
|
||||||
# CommonMark.
|
|
||||||
#
|
|
||||||
# TODO: Permitir una lista de atributos y etiquetas en el Layout
|
|
||||||
#
|
|
||||||
# XXX: Vamos a generar un reproductor de video/audio directamente
|
|
||||||
# desde un plugin de Jekyll
|
|
||||||
def sanitize_options
|
|
||||||
{
|
|
||||||
tags: %w[span],
|
|
||||||
attributes: %w[title class lang]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,4 +4,16 @@ class MetadataDate < MetadataTemplate
|
||||||
def default_value
|
def default_value
|
||||||
Date.today
|
Date.today
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Ver MetadataDocumentDate
|
||||||
|
def value
|
||||||
|
return self[:value] if self[:value].is_a? Date
|
||||||
|
return self[:value] if self[:value].is_a? Time
|
||||||
|
|
||||||
|
begin
|
||||||
|
self[:value] = Date.parse(self[:value] || document.data[name.to_s])
|
||||||
|
rescue ArgumentError, TypeError
|
||||||
|
default_value
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,12 +7,16 @@ class MetadataDocumentDate < MetadataTemplate
|
||||||
Date.today.to_time
|
Date.today.to_time
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# El valor puede ser un Date, Time o una String en el formato
|
||||||
|
# "yyyy-mm-dd"
|
||||||
def value
|
def value
|
||||||
self[:value] || document.date || default_value
|
return self[:value] if self[:value].is_a? Date
|
||||||
end
|
return self[:value] if self[:value].is_a? Time
|
||||||
|
|
||||||
def value=(date)
|
begin
|
||||||
date = date.to_time if date.is_a? String
|
self[:value] = Date.parse(self[:value]).to_time
|
||||||
super(date)
|
rescue ArgumentError, TypeError
|
||||||
|
document.date || default_value
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,6 +38,8 @@ class MetadataFile < MetadataTemplate
|
||||||
|
|
||||||
# Asociar la imagen subida al sitio y obtener la ruta
|
# Asociar la imagen subida al sitio y obtener la ruta
|
||||||
def save
|
def save
|
||||||
|
value['description'] = sanitize(value['description'])
|
||||||
|
|
||||||
return true if uploaded?
|
return true if uploaded?
|
||||||
return true if path_optional?
|
return true if path_optional?
|
||||||
return false unless hardlink.zero?
|
return false unless hardlink.zero?
|
||||||
|
|
|
@ -7,7 +7,7 @@ class MetadataLang < MetadataTemplate
|
||||||
end
|
end
|
||||||
|
|
||||||
def value
|
def value
|
||||||
self[:value] || document.collection.label.to_sym
|
self[:value] || document.collection.label || default_value
|
||||||
end
|
end
|
||||||
|
|
||||||
def values
|
def values
|
||||||
|
|
|
@ -7,4 +7,10 @@ class MetadataOrder < MetadataTemplate
|
||||||
def default_value
|
def default_value
|
||||||
site.posts(lang: post.lang.value).sort_by(:date).index(post)
|
site.posts(lang: post.lang.value).sort_by(:date).index(post)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def save
|
||||||
|
self[:value] = value.to_i
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,8 +39,6 @@ class MetadataSlug < MetadataTemplate
|
||||||
private
|
private
|
||||||
|
|
||||||
def title
|
def title
|
||||||
return if post.title.try(:value).blank?
|
post.title.try(:value) unless post.title.try(:value).blank?
|
||||||
|
|
||||||
post.title.try(:value)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,8 +6,4 @@ class MetadataString < MetadataTemplate
|
||||||
def default_value
|
def default_value
|
||||||
''
|
''
|
||||||
end
|
end
|
||||||
|
|
||||||
def value
|
|
||||||
super.strip
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
:value, :help, :required, :errors, :post,
|
:value, :help, :required, :errors, :post,
|
||||||
:layout, keyword_init: true) do
|
:layout, keyword_init: true) do
|
||||||
|
include ActionText::ContentHelper
|
||||||
|
|
||||||
# El valor por defecto
|
# El valor por defecto
|
||||||
def default_value
|
def default_value
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -22,6 +24,9 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
end
|
end
|
||||||
|
|
||||||
# Valor actual o por defecto
|
# Valor actual o por defecto
|
||||||
|
#
|
||||||
|
# XXX: No estamos sanitizando la entrada, cada tipo tiene que
|
||||||
|
# auto-sanitizarse.
|
||||||
def value
|
def value
|
||||||
self[:value] || document.data.fetch(name.to_s, default_value)
|
self[:value] || document.data.fetch(name.to_s, default_value)
|
||||||
end
|
end
|
||||||
|
@ -60,6 +65,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
# En caso de que algún campo necesite realizar acciones antes de ser
|
# En caso de que algún campo necesite realizar acciones antes de ser
|
||||||
# guardado
|
# guardado
|
||||||
def save
|
def save
|
||||||
|
self[:value] = sanitize value
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -69,5 +75,18 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
def can_be_empty?
|
def can_be_empty?
|
||||||
true unless required && empty?
|
true unless required && empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# No usamos sanitize_action_text_content porque espera un ActionText
|
||||||
|
#
|
||||||
|
# Ver ActionText::ContentHelper#sanitize_action_text_content
|
||||||
|
def sanitize(string)
|
||||||
|
return unless string
|
||||||
|
return string unless string.is_a? String
|
||||||
|
|
||||||
|
sanitizer.sanitize(string,
|
||||||
|
tags: allowed_tags,
|
||||||
|
attributes: allowed_attributes,
|
||||||
|
scrubber: scrubber).strip.html_safe
|
||||||
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/BlockLength
|
# rubocop:enable Metrics/BlockLength
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Un campo de texto largo
|
# Un campo de texto largo
|
||||||
class MetadataText < MetadataString
|
class MetadataText < MetadataString; end
|
||||||
end
|
|
||||||
|
|
|
@ -79,7 +79,6 @@ class Post < OpenStruct
|
||||||
File.mtime(path.absolute)
|
File.mtime(path.absolute)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# Solo ejecuta la magia de OpenStruct si el campo existe en la
|
# Solo ejecuta la magia de OpenStruct si el campo existe en la
|
||||||
# plantilla
|
# plantilla
|
||||||
#
|
#
|
||||||
|
@ -163,10 +162,7 @@ class Post < OpenStruct
|
||||||
def destroy
|
def destroy
|
||||||
FileUtils.rm_f path.absolute
|
FileUtils.rm_f path.absolute
|
||||||
|
|
||||||
# TODO: Devolver self en lugar de todo el array
|
site.delete_post self
|
||||||
site.posts(lang: lang.value).reject! do |post|
|
|
||||||
post.path.absolute == path.absolute
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
alias destroy! destroy
|
alias destroy! destroy
|
||||||
|
|
||||||
|
|
|
@ -187,6 +187,16 @@ class Site < ApplicationRecord
|
||||||
@posts[lang]
|
@posts[lang]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Elimina un artículo de la colección
|
||||||
|
def delete_post(post)
|
||||||
|
lang = post.lang.value
|
||||||
|
|
||||||
|
collections[lang.to_s].docs.delete(post.document) &&
|
||||||
|
posts(lang: lang).delete(post)
|
||||||
|
|
||||||
|
post
|
||||||
|
end
|
||||||
|
|
||||||
# Obtiene todas las plantillas de artículos
|
# Obtiene todas las plantillas de artículos
|
||||||
#
|
#
|
||||||
# @return { post: Layout }
|
# @return { post: Layout }
|
||||||
|
@ -335,7 +345,9 @@ class Site < ApplicationRecord
|
||||||
def deploy_local_presence
|
def deploy_local_presence
|
||||||
# Usamos size porque queremos saber la cantidad de deploys sin
|
# Usamos size porque queremos saber la cantidad de deploys sin
|
||||||
# guardar también
|
# guardar también
|
||||||
return if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal')
|
if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence'))
|
errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence'))
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,5 +34,4 @@
|
||||||
- next if @post.send(attr).front_matter?
|
- next if @post.send(attr).front_matter?
|
||||||
|
|
||||||
%section{ id: attr }
|
%section{ id: attr }
|
||||||
-# TODO: Esto es un agujero de XSS!!!!
|
= @post.send(attr).value.html_safe
|
||||||
= raw @post.send(attr).value
|
|
||||||
|
|
|
@ -151,3 +151,11 @@ datos. De todas formas es información que no tiene sentido almacenar en
|
||||||
información basura.
|
información basura.
|
||||||
|
|
||||||
Nos protege el rate limit, CORS y XSS.
|
Nos protege el rate limit, CORS y XSS.
|
||||||
|
|
||||||
|
## Atención
|
||||||
|
|
||||||
|
* Sanitizar todas las entradas
|
||||||
|
|
||||||
|
* No dejar que se modifique slug, date, orden y otros metadatos internos
|
||||||
|
|
||||||
|
Quizás valga la pena redefinir cuáles son los parámetros anónimos
|
||||||
|
|
|
@ -19,6 +19,7 @@ class EditorTest < ActionDispatch::IntegrationTest
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'al enviar html se guarda markdown' do
|
test 'al enviar html se guarda markdown' do
|
||||||
|
skip
|
||||||
content = <<~CONTENT
|
content = <<~CONTENT
|
||||||
<h1>Hola</h1>
|
<h1>Hola</h1>
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ class EditorTest < ActionDispatch::IntegrationTest
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'convertir trix' do
|
test 'convertir trix' do
|
||||||
|
skip
|
||||||
path = Rails.root.join('test', 'fixtures', 'files')
|
path = Rails.root.join('test', 'fixtures', 'files')
|
||||||
trix_orig = File.read(File.join(path, 'trix.txt'))
|
trix_orig = File.read(File.join(path, 'trix.txt'))
|
||||||
trix_md = File.read(File.join(path, 'trix.md'))
|
trix_md = File.read(File.join(path, 'trix.md'))
|
||||||
|
|
|
@ -28,7 +28,7 @@ class PostTest < ActiveSupport::TestCase
|
||||||
test 'se pueden eliminar' do
|
test 'se pueden eliminar' do
|
||||||
assert @post.destroy
|
assert @post.destroy
|
||||||
assert_not File.exist?(@post.path.absolute)
|
assert_not File.exist?(@post.path.absolute)
|
||||||
assert_not @site.posts.include?(@post)
|
assert_not @site.posts(lang: @post.lang.value).include?(@post)
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'se puede ver el contenido completo después de guardar' do
|
test 'se puede ver el contenido completo después de guardar' do
|
||||||
|
|
Loading…
Reference in a new issue