3 Commits

Author SHA1 Message Date
e11686f4a0 feat(http): implement origin and referer validation for HTTP requests
Some checks failed
Build linux_amd64 extension and upload to Packages / build-linux-amd64 (push) Failing after 25m14s
Added helper functions to validate request origins and referers, allowing for configurable local URLs and support for environments where the UI is exposed externally. Introduced environment variable `ui_allow_any_origin` for bypassing origin checks. Updated request handling methods to utilize these validations, enhancing security for HTTP interactions.
2025-09-13 19:02:04 +00:00
5821ee7fc8 feat(ci): add DuckDB binary upload to GitHub Actions workflow
Some checks failed
Build linux_amd64 extension and upload to Packages / build-linux-amd64 (push) Has been cancelled
2025-09-13 18:43:59 +00:00
c52ad95c35 chore(ci): squash history into single commit for GitHub Action setup
All checks were successful
Build linux_amd64 extension and upload to Packages / build-linux-amd64 (push) Successful in 25m21s
2025-09-13 18:01:59 +00:00
2 changed files with 147 additions and 12 deletions

View File

@@ -53,6 +53,46 @@ jobs:
apt-get install -y --no-install-recommends \
build-essential cmake ninja-build python3 python3-venv pkg-config \
libssl-dev curl git ca-certificates
- name: Preflight Gitea upload (fast-fail)
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
: "${GITEA_TOKEN:?GITEA_TOKEN secret is required}"
server="${GITHUB_SERVER_URL:-${{ github.server_url }}}"
owner="${GITHUB_REPOSITORY_OWNER:-${{ github.repository_owner }}}"
repo_full="${GITHUB_REPOSITORY:-${{ github.repository }}}"
pkg="${repo_full##*/}-preflight"
version="preflight-${GITHUB_RUN_ID:-0}-${GITHUB_RUN_ATTEMPT:-0}-$(date +%s)"
name="check.bin"
tmpfile="$(mktemp)"
printf "auth check %s\n" "$(date -u +%FT%TZ)" > "$tmpfile"
# Normalize server to effective scheme+host (handles http->https redirects)
base_no_trail="$(echo "$server" | sed 's#/*$##')"
# Use GET (not HEAD) to avoid servers that don't support HEAD on this endpoint
effective_version_url=$(curl -sS -L -o /dev/null -w '%{url_effective}' "$base_no_trail/api/v1/version" || echo "")
normalized_server=$(echo "$effective_version_url" | sed -E 's#^(https?://[^/]+).*$#\1#')
if [ -n "$normalized_server" ]; then
server="$normalized_server"
fi
url="$server/api/packages/$owner/generic/$pkg/$version/$name?replace=1"
auth_user="${ACTOR:-$owner}"
echo "Preflight: server=$server owner=$owner package=$pkg version=$version"
# Perform preflight upload using Basic auth directly
if curl -fS -L -X PUT \
-u "$auth_user:${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--upload-file "$tmpfile" "$url" >/dev/null; then
echo "Preflight upload succeeded, cleaning up"
else
echo "Preflight upload failed" >&2
exit 1
fi
# Cleanup the uploaded dummy package version (best effort)
curl -sS -L -o /dev/null -w " delete -> HTTP %{http_code}\n" \
-u "$auth_user:${GITEA_TOKEN}" -X DELETE \
"$server/api/packages/$owner/generic/$pkg/$version" || true
- name: Initialize submodules
run: |
set -e
@@ -96,7 +136,8 @@ jobs:
echo "Using version: $version"
- name: Upload to Gitea Packages (generic)
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
: "${GITEA_TOKEN:?GITEA_TOKEN secret is required}"
@@ -127,11 +168,39 @@ jobs:
[ -n "$pkg" ] || { echo "pkg not set" >&2; exit 1; }
[ -n "$version" ] || { echo "version not set" >&2; exit 1; }
[ -n "$file" ] || { echo "file not set" >&2; exit 1; }
# Normalize server using effective URL of /api/v1/version (handles http->https)
base_no_trail="$(echo "$server" | sed 's#/*$##')"
effective_version_url=$(curl -sS -L -o /dev/null -w '%{url_effective}' "$base_no_trail/api/v1/version" || echo "")
normalized_server=$(echo "$effective_version_url" | sed -E 's#^(https?://[^/]+).*$#\1#')
if [ -n "$normalized_server" ]; then
server="$normalized_server"
fi
# Use the GitHub actor as basic auth username by default
auth_user="${ACTOR:-$owner}"
name="$(basename "$file")"
url="$server/api/packages/$owner/generic/$pkg/$version/$name?replace=1"
echo "Uploading $file to $url"
curl -fsSL -X PUT \
-H "Authorization: token ${GITEA_TOKEN}" \
echo " auth user=$auth_user"
# Use Basic auth directly (works with package registry)
curl -fS -L -X PUT \
-u "$auth_user:${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--retry 2 --retry-delay 2 --max-time 300 \
--upload-file "$file" "$url"
echo "Upload complete."
echo "Upload complete."
# Also upload the DuckDB shell binary
bin_path="./build/release/duckdb"
if [ ! -f "$bin_path" ]; then
echo "duckdb binary not found at $bin_path" >&2
exit 1
fi
bin_name="$(basename "$bin_path")"
bin_url="$server/api/packages/$owner/generic/$pkg/$version/$bin_name?replace=1"
echo "Uploading $bin_path to $bin_url"
curl -fS -L -X PUT \
-u "$auth_user:${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--retry 2 --retry-delay 2 --max-time 300 \
--upload-file "$bin_path" "$bin_url"
echo "DuckDB binary upload complete."

View File

@@ -22,6 +22,76 @@ namespace ui {
unique_ptr<HttpServer> HttpServer::server_instance;
// Helpers for validating request origin/referer in deployments where the UI is
// exposed on a non-localhost host (e.g., Docker, k8s, reverse proxies). These
// checks allow either the configured local_url, or the runtime host derived
// from the request headers. They also allow an escape hatch via the
// environment variable `ui_allow_any_origin=1|true`.
namespace {
// Returns true if the given referer begins with any of the expected base URLs.
static bool RefererStartsWithAny(const std::string &referer,
const std::vector<std::string> &bases) {
for (const auto &base : bases) {
if (!base.empty() && referer.compare(0, base.size(), base) == 0) {
return true;
}
}
return false;
}
static std::vector<std::string>
ExpectedBaseUrls(const httplib::Request &req, const std::string &local_url) {
// Prefer forwarded host if present, otherwise fall back to Host.
auto forwarded_host = req.get_header_value("X-Forwarded-Host");
auto host = forwarded_host.empty() ? req.get_header_value("Host")
: forwarded_host;
std::vector<std::string> bases;
bases.push_back(local_url);
if (!host.empty()) {
bases.push_back(StringUtil::Format("http://%s", host));
bases.push_back(StringUtil::Format("https://%s", host));
}
return bases;
}
static bool IsOriginAllowed(const httplib::Request &req,
const std::string &local_url) {
if (IsEnvEnabled("ui_allow_any_origin")) {
return true;
}
auto origin = req.get_header_value("Origin");
if (origin.empty()) {
return false;
}
auto bases = ExpectedBaseUrls(req, local_url);
for (const auto &base : bases) {
if (origin == base) {
return true;
}
}
return false;
}
static bool IsRefererAllowed(const httplib::Request &req,
const std::string &local_url) {
if (IsEnvEnabled("ui_allow_any_origin")) {
return true;
}
auto referer = req.get_header_value("Referer");
if (referer.empty()) {
return false;
}
return RefererStartsWithAny(referer, ExpectedBaseUrls(req, local_url));
}
} // namespace
HttpServer *HttpServer::GetInstance(ClientContext &context) {
if (server_instance) {
// We already have an instance, make sure we're running on the right DB
@@ -228,8 +298,7 @@ void HttpServer::HandleGetLocalToken(const httplib::Request &req,
httplib::Response &res) {
// GET requests don't include Origin, so use Referer instead.
// Referer includes the path, so only compare the start.
auto referer = req.get_header_value("Referer");
if (referer.compare(0, local_url.size(), local_url) != 0) {
if (!IsRefererAllowed(req, local_url)) {
res.status = 401;
return;
}
@@ -321,8 +390,7 @@ void HttpServer::HandleGet(const httplib::Request &req,
void HttpServer::HandleInterrupt(const httplib::Request &req,
httplib::Response &res) {
auto origin = req.get_header_value("Origin");
if (origin != local_url) {
if (!IsOriginAllowed(req, local_url)) {
res.status = 401;
return;
}
@@ -361,8 +429,7 @@ void HttpServer::HandleRun(const httplib::Request &req, httplib::Response &res,
void HttpServer::DoHandleRun(const httplib::Request &req,
httplib::Response &res,
const httplib::ContentReader &content_reader) {
auto origin = req.get_header_value("Origin");
if (origin != local_url) {
if (!IsOriginAllowed(req, local_url)) {
res.status = 401;
return;
}
@@ -625,8 +692,7 @@ void HttpServer::DoHandleRun(const httplib::Request &req,
void HttpServer::HandleTokenize(const httplib::Request &req,
httplib::Response &res,
const httplib::ContentReader &content_reader) {
auto origin = req.get_header_value("Origin");
if (origin != local_url) {
if (!IsOriginAllowed(req, local_url)) {
res.status = 401;
return;
}