مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2026-06-03 21:45:48 +00:00
Webhook and HTTP message endpoint deliveries both flow through Postal::HTTP, which parsed the user-supplied URL and connected to its host with no address validation. An authenticated user could point a webhook or endpoint at a private, loopback or link-local address (e.g. 127.0.0.1, 169.254.169.254 cloud metadata, RFC1918 hosts) and make the server issue requests into its own internal network. Add Postal::HTTP::AddressGuard, which resolves the destination host and rejects private/loopback/link-local/reserved/multicast IPv4 and IPv6 addresses, then pins the connection to the validated address so it cannot be redirected via a DNS-rebinding race. Administrators can permit specific destinations via the new postal.allowed_request_destinations config option (hostnames or IP/CIDR ranges). Address selection only uses families this server can actually reach so we do not pin to an IPv6 address on a host without IPv6 connectivity; IPv4 is preferred for predictability. HTTPEndpoint now validates that its URL is a well-formed HTTP(S) URL with a host.
76 أسطر
1.8 KiB
Ruby
76 أسطر
1.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "uri"
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: http_endpoints
|
|
#
|
|
# id :integer not null, primary key
|
|
# server_id :integer
|
|
# uuid :string(255)
|
|
# name :string(255)
|
|
# url :string(255)
|
|
# encoding :string(255)
|
|
# format :string(255)
|
|
# strip_replies :boolean default(FALSE)
|
|
# error :text(65535)
|
|
# disabled_until :datetime
|
|
# last_used_at :datetime
|
|
# created_at :datetime
|
|
# updated_at :datetime
|
|
# include_attachments :boolean default(TRUE)
|
|
# timeout :integer
|
|
#
|
|
|
|
class HTTPEndpoint < ApplicationRecord
|
|
|
|
DEFAULT_TIMEOUT = 5
|
|
|
|
include HasUUID
|
|
|
|
belongs_to :server
|
|
has_many :routes, as: :endpoint
|
|
has_many :additional_route_endpoints, dependent: :destroy, as: :endpoint
|
|
|
|
ENCODINGS = %w[BodyAsJSON FormData].freeze
|
|
FORMATS = %w[Hash RawMessage].freeze
|
|
|
|
before_destroy :update_routes
|
|
|
|
validates :name, presence: true
|
|
validates :url, presence: true
|
|
validate :url_must_be_http_or_https
|
|
validates :encoding, inclusion: { in: ENCODINGS }
|
|
validates :format, inclusion: { in: FORMATS }
|
|
validates :timeout, numericality: { greater_than_or_equal_to: 5, less_than_or_equal_to: 60 }
|
|
|
|
default_value :timeout, -> { DEFAULT_TIMEOUT }
|
|
|
|
def description
|
|
"#{name} (#{url})"
|
|
end
|
|
|
|
def mark_as_used
|
|
update_column(:last_used_at, Time.now)
|
|
end
|
|
|
|
def update_routes
|
|
routes.each { |r| r.update(endpoint: nil, mode: "Reject") }
|
|
end
|
|
|
|
private
|
|
|
|
def url_must_be_http_or_https
|
|
return if url.blank?
|
|
|
|
uri = URI.parse(url)
|
|
return if uri.is_a?(URI::HTTP) && uri.host.present?
|
|
|
|
errors.add(:url, "must be an HTTP or HTTPS URL")
|
|
rescue URI::InvalidURIError
|
|
errors.add(:url, "must be a valid HTTP or HTTPS URL")
|
|
end
|
|
|
|
end
|