SECURITY: Place a SSRF protection when calling services from the plugin. (#485)

The Faraday adapter and `FinalDestionation::HTTP` will protect us from admin-initiated SSRF attacks when interacting with the external services powering this plugin features.:
This commit is contained in:
Roman Rizzi 2024-02-21 17:14:50 -03:00 committed by Selase Krakani
parent 1f74a77e17
commit a65c4076a6
No known key found for this signature in database
GPG Key ID: AF2495CE8CAB3CEE
10 changed files with 21 additions and 14 deletions

View File

@ -69,7 +69,7 @@ module DiscourseAi
prompt = dialect.translate prompt = dialect.translate
Net::HTTP.start( FinalDestination::HTTP.start(
model_uri.host, model_uri.host,
model_uri.port, model_uri.port,
use_ssl: true, use_ssl: true,

View File

@ -14,7 +14,8 @@ module ::DiscourseAi
endpoint = "#{base_url}#{model}" endpoint = "#{base_url}#{model}"
response = Faraday.post(endpoint, content.to_json, headers) conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
response = conn.post(endpoint, content.to_json, headers)
raise Net::HTTPBadResponse if ![200].include?(response.status) raise Net::HTTPBadResponse if ![200].include?(response.status)

View File

@ -8,7 +8,8 @@ module ::DiscourseAi
headers["X-API-KEY"] = api_key if api_key.present? headers["X-API-KEY"] = api_key if api_key.present?
response = Faraday.post(endpoint, { model: model, content: content }.to_json, headers) conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
response = conn.post(endpoint, { model: model, content: content }.to_json, headers)
raise Net::HTTPBadResponse if ![200, 415].include?(response.status) raise Net::HTTPBadResponse if ![200, 415].include?(response.status)

View File

@ -8,8 +8,9 @@ module ::DiscourseAi
headers["X-API-KEY"] = api_key if api_key.present? headers["X-API-KEY"] = api_key if api_key.present?
conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
response = response =
Faraday.post( conn.post(
endpoint, endpoint,
{ model: model, content: content, candidates: candidates }.to_json, { model: model, content: content, candidates: candidates }.to_json,
headers, headers,

View File

@ -11,7 +11,8 @@ module ::DiscourseAi
body = { content: { parts: [{ text: content }] } } body = { content: { parts: [{ text: content }] } }
response = Faraday.post(url, body.to_json, headers) conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
response = conn.post(url, body.to_json, headers)
raise Net::HTTPBadResponse if ![200].include?(response.status) raise Net::HTTPBadResponse if ![200].include?(response.status)

View File

@ -18,7 +18,8 @@ module ::DiscourseAi
headers["X-API-KEY"] = SiteSetting.ai_hugging_face_tei_api_key headers["X-API-KEY"] = SiteSetting.ai_hugging_face_tei_api_key
end end
response = Faraday.post(api_endpoint, body, headers) conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
response = conn.post(api_endpoint, body, headers)
raise Net::HTTPBadResponse if ![200].include?(response.status) raise Net::HTTPBadResponse if ![200].include?(response.status)

View File

@ -15,7 +15,8 @@ module ::DiscourseAi
payload = { model: model, input: content } payload = { model: model, input: content }
payload[:dimensions] = dimensions if dimensions.present? payload[:dimensions] = dimensions if dimensions.present?
response = Faraday.post(SiteSetting.ai_openai_embeddings_url, payload.to_json, headers) conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
response = conn.post(SiteSetting.ai_openai_embeddings_url, payload.to_json, headers)
case response.status case response.status
when 200 when 200

View File

@ -28,7 +28,7 @@ module ::DiscourseAi
response_format: "b64_json", response_format: "b64_json",
} }
Net::HTTP.start( FinalDestination::HTTP.start(
uri.host, uri.host,
uri.port, uri.port,
use_ssl: uri.scheme == "https", use_ssl: uri.scheme == "https",

View File

@ -57,7 +57,8 @@ module ::DiscourseAi
endpoint = "v1/generation/#{engine}/text-to-image" endpoint = "v1/generation/#{engine}/text-to-image"
response = Faraday.post("#{api_url}/#{endpoint}", payload.to_json, headers) conn = Faraday.new { |f| f.adapter FinalDestination::FaradayAdapter }
response = conn.post("#{api_url}/#{endpoint}", payload.to_json, headers)
if response.status != 200 if response.status != 200
Rails.logger.error( Rails.logger.error(

View File

@ -99,13 +99,13 @@ class EndpointMock
def with_chunk_array_support def with_chunk_array_support
mock = mocked_http mock = mocked_http
@original_net_http = ::Net.send(:remove_const, :HTTP) @original_net_http = ::FinalDestination.send(:remove_const, :HTTP)
::Net.send(:const_set, :HTTP, mock) ::FinalDestination.send(:const_set, :HTTP, mock)
yield yield
ensure ensure
::Net.send(:remove_const, :HTTP) ::FinalDestination.send(:remove_const, :HTTP)
::Net.send(:const_set, :HTTP, @original_net_http) ::FinalDestination.send(:const_set, :HTTP, @original_net_http)
end end
protected protected
@ -113,7 +113,7 @@ class EndpointMock
# Copied from https://github.com/bblimke/webmock/issues/629 # Copied from https://github.com/bblimke/webmock/issues/629
# Workaround for stubbing a streamed response # Workaround for stubbing a streamed response
def mocked_http def mocked_http
Class.new(::Net::HTTP) do Class.new(FinalDestination::HTTP) do
def request(*) def request(*)
super do |response| super do |response|
response.instance_eval do response.instance_eval do