defmodule GitGud.OutboundUrlTest do
# Touches the global OutboundUrl config.
use GitGud.DataCase, async: false
use Oban.Testing, repo: GitGud.Repo
alias GitGud.Federation.RemoteActors
alias GitGud.Imports
alias GitGud.OutboundUrl
alias GitGud.Repositories
alias GitGud.Webhooks
alias GitGud.Webhooks.WebhookDelivery
alias GitGud.Webhooks.Workers.DeliverWebhook
import GitGud.AccountsFixtures
import GitGud.ForgeFixtures
setup do
prev = Application.get_env(:git_gud, OutboundUrl, [])
on_exit(fn -> Application.put_env(:git_gud, OutboundUrl, prev) end)
{:ok, prev: prev}
end
defp put_config(prev, kv),
do: Application.put_env(:git_gud, OutboundUrl, Keyword.merge(prev, kv))
describe "check/3" do
test "refuses loopback, link-local, private, CGNAT and reserved addresses" do
for url <- [
"http://127.0.0.1/",
"http://127.1:8080/",
"http://2130706433/",
"http://169.254.169.254/latest/meta-data/",
"http://10.0.0.1/",
"http://172.16.5.4/",
"http://192.168.1.1/",
"http://100.64.0.1/",
"http://0.0.0.0/",
"http://[::1]/",
"http://[::ffff:127.0.0.1]/",
"http://[::ffff:169.254.169.254]/",
"http://[fc00::1]/",
"http://[fd12:3456::1]/",
"http://[fe80::1]/",
"http://[64:ff9b::a00:1]/",
"http://localhost/",
"https://db.internal.test/",
# One private answer among public ones is enough to refuse.
"https://dual.test/"
] do
assert {:error, {:blocked_address, _}} = OutboundUrl.check(url, :import), url
end
end
test "accepts public hosts and addresses" do
assert {:ok, %{host: "github.com", port: 443}} =
OutboundUrl.check("https://github.com/acme/x.git", :import)
assert {:ok, _} = OutboundUrl.check("http://8.8.8.8/", :webhook)
assert {:ok, _} = OutboundUrl.check("http://[2606:4700::1111]/", :federation)
end
test "refuses other schemes and junk" do
assert {:error, :unsupported_scheme} = OutboundUrl.check("file:///etc/passwd", :import)
assert {:error, :unsupported_scheme} = OutboundUrl.check("ftp://example.com/", :import)
assert {:error, :unsupported_scheme} = OutboundUrl.check("gopher://example.com/", :import)
assert {:error, _} = OutboundUrl.check("/var/lib/repos/x.git", :import)
assert {:error, :invalid_url} = OutboundUrl.check(nil, :import)
assert {:error, :nxdomain} = OutboundUrl.check("https://nowhere.test/", :import)
end
test "the operator override lifts the private rule for one purpose only", %{prev: prev} do
put_config(prev, allow_private: [:import])
assert {:ok, _} = OutboundUrl.check("http://192.168.1.10/", :import)
assert {:error, {:blocked_address, _}} = OutboundUrl.check("http://192.168.1.10/", :webhook)
# The scheme rule still applies.
assert {:error, :unsupported_scheme} = OutboundUrl.check("file:///etc/passwd", :import)
end
end
describe "git_config/2" do
test "pins http(s) remotes to the checked address and turns redirects off" do
assert {:ok, args} = OutboundUrl.git_config("https://example.com/a/b.git", :import)
assert "http.followRedirects=false" in args
assert "protocol.allow=never" in args
assert "http.curloptResolve=example.com:443:93.184.216.34" in args
end
test "refuses on-disk paths and file:// outside of the test fixture switch", %{prev: prev} do
put_config(prev, allow_local_paths: false)
assert {:error, _} = OutboundUrl.git_config("/srv/repos/someone/private.git", :import)
assert {:error, _} = OutboundUrl.git_config("file:///srv/repos/x.git", :import)
assert {:error, _} = OutboundUrl.git_config("--upload-pack=touch /tmp/x", :import)
assert {:error, _} = OutboundUrl.git_config("ext::sh -c whoami", :import)
assert {:error, {:blocked_address, _}} =
OutboundUrl.git_config("http://10.0.0.1/x.git", :import)
end
end
describe "attach/2" do
test "a refused URL never reaches the adapter" do
test_pid = self()
req =
Req.new(url: "http://169.254.169.254/latest/meta-data/", retry: false)
|> Req.merge(
plug: fn conn ->
send(test_pid, :reached)
Plug.Conn.resp(conn, 200, "")
end
)
|> OutboundUrl.attach(:import)
assert {:error, %OutboundUrl.Blocked{reason: {:blocked_address, "169.254.169.254"}}} =
Req.get(req)
refute_received :reached
end
test "allowed URLs go through, and redirects are not followed" do
req =
Req.new(url: "https://example.com/start", retry: false)
|> Req.merge(
plug: fn conn ->
conn
|> Plug.Conn.put_resp_header("location", "http://169.254.169.254/")
|> Plug.Conn.resp(302, "")
end
)
|> OutboundUrl.attach(:import)
assert {:ok, %Req.Response{status: 302}} = Req.get(req)
end
end
describe "call sites" do
test "a GitHub discovery against a private API root is refused before any request" do
test_pid = self()
Application.put_env(:git_gud, :imports_req_options,
plug: fn conn ->
send(test_pid, :reached)
Plug.Conn.resp(conn, 200, "[]")
end
)
on_exit(fn -> Application.delete_env(:git_gud, :imports_req_options) end)
assert {:error, {:outbound, {:blocked_address, "169.254.169.254"}}} =
Imports.discover(%{
"provider" => "github",
"remote_owner" => "acme",
"base_url" => "http://169.254.169.254"
})
refute_received :reached
end
test "a provider can't hand back a clone URL on the private network" do
Application.put_env(:git_gud, :imports_req_options,
plug: fn conn ->
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.resp(
200,
Jason.encode!([
%{"name" => "x", "full_name" => "acme/x", "clone_url" => "http://10.0.0.9/x.git"}
])
)
end
)
on_exit(fn -> Application.delete_env(:git_gud, :imports_req_options) end)
assert {:error, {:outbound, {:blocked_address, "10.0.0.9"}}} =
Imports.discover(%{"provider" => "github", "remote_owner" => "acme"})
end
test "a single-URL import of an internal host is refused at discovery" do
assert {:error, {:outbound, {:blocked_address, _}}} =
Imports.discover(%{
"provider" => "git",
"remote_owner" => "https://git.internal.test/team/x.git"
})
end
test "start_import refuses a private clone URL even if discovery was skipped" do
user = user_fixture()
repo = %{
name: "x",
full_name: "acme/x",
clone_url: "http://169.254.169.254/x.git",
description: nil,
default_branch: nil,
private: true,
fork: false,
archived: false
}
assert {:error, {:outbound, _}} =
Imports.start_import(
user,
user,
%{"provider" => "git", "remote_owner" => repo.clone_url},
[repo],
nil
)
end
test "clone_addr can't name another repository on this server's disk", %{prev: prev} do
put_config(prev, allow_local_paths: false)
{_owner, victim} = repository_fixture()
user = user_fixture()
for addr <- [victim.disk_path, "file://" <> victim.disk_path, "http://127.0.0.1:1/x.git"] do
assert {:error, {:git_init, {:git_clone, _}}} =
Repositories.create_repository(user, %{
"name" => "steal#{System.unique_integer([:positive])}",
"clone_addr" => addr
}),
addr
end
end
test "upstream sync refuses a private upstream", %{prev: prev} do
put_config(prev, allow_local_paths: false)
{_owner, repo} = repository_fixture()
repo =
repo
|> Ecto.Changeset.change(upstream_url: "http://169.254.169.254/x.git")
|> GitGud.Repo.update!()
assert {:error, {:git_fetch, msg}} = Repositories.sync_upstream(repo)
assert msg =~ "private or reserved"
end
test "a webhook to a private address is logged as failed without being sent" do
{_user, repo} = repository_fixture()
{:ok, hook} = Webhooks.create_webhook(repo, %{"url" => "http://169.254.169.254/latest"})
assert {:error, _} =
perform_job(DeliverWebhook, %{
"webhook_id" => hook.id,
"event" => "push",
"payload" => %{}
})
[delivery] = GitGud.Repo.all(WebhookDelivery)
assert delivery.status == "failure"
assert delivery.status_code == nil
assert delivery.response_body =~ "Blocked"
end
test "an ActivityPub fetch of a private address is refused" do
assert {:error, %OutboundUrl.Blocked{}} =
RemoteActors.fetch("http://169.254.169.254/users/alice")
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
9.1 KiB · text
History
49fdeb3