validator.rb 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. module TriggerWebhookJob::CustomPayload::Validator
  3. # This module validates replacement variables if there executed reference
  4. # object or method is allowed. This prevents the execution of arbitrary
  5. # code.
  6. private
  7. ALLOWED_SIMPLE_CLASSES = %w[
  8. Integer
  9. String
  10. Float
  11. FalseClass
  12. TrueClass
  13. ].freeze
  14. ALLOWED_RAILS_CLASSES = %w[
  15. ActiveSupport::TimeWithZone
  16. ActiveSupport::Duration
  17. ].freeze
  18. ALLOWED_CONTAINER_CLASSES = %w[
  19. Hash
  20. Array
  21. ].freeze
  22. ALLOWED_DEFAULT_CLASSES = ALLOWED_SIMPLE_CLASSES + ALLOWED_RAILS_CLASSES
  23. # This method executes the replacement variables and executes on any error,
  24. # e.g. no such method, no such object, missing method, etc. the error is
  25. # added to the mappping.
  26. def validate_methods!(methods, reference, display)
  27. return "\#{#{display} / missing method}" if methods.blank?
  28. methods.each_with_index do |method, index|
  29. display = "#{display}.#{method}"
  30. result = validate_method!(method, reference, display)
  31. return result if !result.nil?
  32. begin
  33. value = reference.send(method)
  34. rescue => e
  35. return "\#{#{display} / #{e.message}}"
  36. end
  37. return '' if value.nil?
  38. return validate_value!(value, display) if index == methods.size - 1
  39. reference = value
  40. end
  41. end
  42. # Final value must be one of the above described classes.
  43. def validate_value!(value, display)
  44. return validate_container_values(value) if value.class.to_s.in?(ALLOWED_CONTAINER_CLASSES)
  45. return "\#{#{display} / no such method}" if !value.class.to_s.in?(ALLOWED_DEFAULT_CLASSES)
  46. value
  47. end
  48. def validate_container_values(container)
  49. case container.class.to_s
  50. when 'Array'
  51. container.each_with_index do |value, index|
  52. container[index] = value.class.to_s.in?(ALLOWED_DEFAULT_CLASSES) ? value : 'no such item'
  53. end
  54. when 'Hash'
  55. container.each do |key, value|
  56. container[key] = value.class.to_s.in?(ALLOWED_DEFAULT_CLASSES) ? value : 'no such item'
  57. end
  58. end
  59. container
  60. end
  61. # Any top level object must be provided by the tracks hash (ticket, article,
  62. # notification by default, any further information is related to the webhook
  63. # content).
  64. def validate_object!(object, tracks)
  65. return "\#{no object provided}" if object.blank?
  66. return "\#{#{object} / no such object}" if tracks.keys.exclude?(object.to_sym)
  67. nil
  68. end
  69. # Validate the next method to be called.
  70. def validate_method!(method, reference, display)
  71. return "\#{#{display} / missing method}" if method.blank?
  72. # Inspecting a symbol quotes invalid method names.
  73. return "\#{#{display} / no such method}" if method.to_sym.inspect.start_with?(%r{:"@?})
  74. return "\#{#{display} / no such method}" if !allowed_class_method?(method, reference)
  75. return "\#{#{display} / no such method}" if !reference.respond_to?(method.to_sym)
  76. nil
  77. end
  78. # This method verfies the class of a referenced object or the next method to
  79. # be called.
  80. def allowed_class_method?(method, reference)
  81. klass = reference.class.to_s
  82. # If the referenced object is one of the allowed simple classes no further
  83. # validation is required.
  84. return true if klass.in?(ALLOWED_DEFAULT_CLASSES)
  85. # The next method to be called must be explicit allowed within the
  86. # referenced track classes.
  87. tracks.select { |t| t.klass == klass }.each do |track|
  88. return true if track.functions.include?(method)
  89. end
  90. false
  91. end
  92. # This method verifies that the replaced custom payload is valid JSON.
  93. # This is done back and forth because the strictness of the JSON parser
  94. # is laxer than the JSON generator.
  95. def valid!(record)
  96. JSON.parse(record).to_json
  97. end
  98. end