validator.rb 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  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. ].freeze
  12. ALLOWED_RAILS_CLASSES = %w[
  13. ActiveSupport::TimeWithZone
  14. ActiveSupport::Duration
  15. ].freeze
  16. ALLOWED_DEFAULT_CLASSES = ALLOWED_SIMPLE_CLASSES + ALLOWED_RAILS_CLASSES
  17. # This method executes the replacement variables and executes on any error,
  18. # e.g. no such method, no such object, missing method, etc. the error is
  19. # added to the mappping.
  20. def validate_methods!(methods, reference, display)
  21. return "\#{#{display} / missing method}" if methods.blank?
  22. methods.each_with_index do |method, index|
  23. display = "#{display}.#{method}"
  24. result = validate_method!(method, reference, display)
  25. return result if !result.nil?
  26. begin
  27. value = reference.send(method)
  28. rescue => e
  29. return "\#{#{display} / #{e.message}}"
  30. end
  31. return '' if value.nil?
  32. return validate_value!(value, display) if index == methods.size - 1
  33. reference = value
  34. end
  35. end
  36. # Final value must be one of the above described classes.
  37. def validate_value!(value, display)
  38. return "\#{#{display} / no such method}" if !value.class.to_s.in?(ALLOWED_DEFAULT_CLASSES)
  39. value
  40. end
  41. # Any top level object must be provided by the tracks hash (ticket, article,
  42. # notification by default, any further information is related to the webhook
  43. # content).
  44. def validate_object!(object, tracks)
  45. return "\#{no object provided}" if object.blank?
  46. return "\#{#{object} / no such object}" if tracks.keys.exclude?(object.to_sym)
  47. nil
  48. end
  49. # Validate the next method to be called.
  50. def validate_method!(method, reference, display)
  51. return "\#{#{display} / missing method}" if method.blank?
  52. return "\#{#{display} / no such method}" if !allowed_class_method?(method, reference)
  53. return "\#{#{display} / no such method}" if !reference.respond_to?(method.to_sym)
  54. nil
  55. end
  56. # This method verfies the class of a referenced object or the next method to
  57. # be called.
  58. def allowed_class_method?(method, reference)
  59. klass = reference.class.to_s
  60. # If the referenced object is one of the allowed simple classes no further
  61. # validation is required.
  62. return true if klass.in?(ALLOWED_DEFAULT_CLASSES)
  63. # The next method to be called must be explicit allowed within the
  64. # referenced track classes.
  65. tracks.select { |t| t.klass == klass }.each do |track|
  66. return true if track.functions.include?(method)
  67. end
  68. false
  69. end
  70. # This method verifies that the replaced custom payload is valid JSON.
  71. # This is done back and forth because the strictness of the JSON parser
  72. # is laxer than the JSON generator.
  73. def valid!(record)
  74. JSON.parse(record).to_json
  75. end
  76. end