From 73b6077ac386c7ae7fd87a82bcb1cfc81bf27b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Tue, 12 May 2026 12:18:17 +0200 Subject: [PATCH] Constrain Lunchflow base URL to trusted endpoint (#1768) * Constrain Lunchflow base URL to trusted endpoint Prevent SSRF by ignoring user-provided Lunchflow base_url values unless they match the canonical Lunchflow HTTPS endpoint. Add model tests covering invalid host/scheme and valid canonicalization behavior. * Linter --- app/models/lunchflow_item.rb | 15 ++++++++++++++- test/models/lunchflow_item_test.rb | 31 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 test/models/lunchflow_item_test.rb diff --git a/app/models/lunchflow_item.rb b/app/models/lunchflow_item.rb index ba9830c6b..94cf078e7 100644 --- a/app/models/lunchflow_item.rb +++ b/app/models/lunchflow_item.rb @@ -1,6 +1,8 @@ class LunchflowItem < ApplicationRecord include Syncable, Provided, Unlinking, Encryptable + DEFAULT_BASE_URL = "https://lunchflow.app/api/v1".freeze + enum :status, { good: "good", requires_update: "requires_update" }, default: :good # Encrypt sensitive credentials and raw payloads if ActiveRecord encryption is configured @@ -154,6 +156,17 @@ class LunchflowItem < ApplicationRecord end def effective_base_url - base_url.presence || "https://lunchflow.app/api/v1" + return DEFAULT_BASE_URL if base_url.blank? + + uri = URI.parse(base_url) + return DEFAULT_BASE_URL unless uri.is_a?(URI::HTTPS) + return DEFAULT_BASE_URL unless uri.host == "lunchflow.app" + return DEFAULT_BASE_URL unless [ "", "/", "/api/v1", "/api/v1/" ].include?(uri.path) + return DEFAULT_BASE_URL unless uri.query.blank? + return DEFAULT_BASE_URL unless uri.fragment.blank? + + DEFAULT_BASE_URL + rescue URI::InvalidURIError + DEFAULT_BASE_URL end end diff --git a/test/models/lunchflow_item_test.rb b/test/models/lunchflow_item_test.rb new file mode 100644 index 000000000..13b06f2fd --- /dev/null +++ b/test/models/lunchflow_item_test.rb @@ -0,0 +1,31 @@ +require "test_helper" + +class LunchflowItemTest < ActiveSupport::TestCase + def setup + @lunchflow_item = lunchflow_items(:one) + end + + test "effective_base_url returns default when base_url blank" do + @lunchflow_item.base_url = nil + + assert_equal LunchflowItem::DEFAULT_BASE_URL, @lunchflow_item.effective_base_url + end + + test "effective_base_url returns default for non-lunchflow host" do + @lunchflow_item.base_url = "https://169.254.169.254/latest/meta-data" + + assert_equal LunchflowItem::DEFAULT_BASE_URL, @lunchflow_item.effective_base_url + end + + test "effective_base_url returns default for non-https scheme" do + @lunchflow_item.base_url = "http://lunchflow.app/api/v1" + + assert_equal LunchflowItem::DEFAULT_BASE_URL, @lunchflow_item.effective_base_url + end + + test "effective_base_url returns canonical default for valid lunchflow url" do + @lunchflow_item.base_url = "https://lunchflow.app/api/v1/" + + assert_equal LunchflowItem::DEFAULT_BASE_URL, @lunchflow_item.effective_base_url + end +end