fix: validate trusted hosts for local api server

This commit is contained in:
David 2025-06-04 00:09:41 +07:00
parent 7dc51c5e0f
commit b674a521f2
5 changed files with 107 additions and 1 deletions

View File

@ -347,8 +347,10 @@ pub async fn start_server(
host: String,
port: u16,
prefix: String,
trusted_hosts: Vec<String>,
) -> Result<bool, String> {
server::start_server(host, port, prefix, app_token(app.state()).unwrap())
let auth_token = app.state::<AppState>().app_token.clone().unwrap_or_default();
server::start_server(host, port, prefix, auth_token, trusted_hosts)
.await
.map_err(|e| e.to_string())?;
Ok(true)

View File

@ -19,6 +19,7 @@ struct ProxyConfig {
upstream: String,
prefix: String,
auth_token: String,
trusted_hosts: Vec<String>,
}
/// Removes a prefix from a path, ensuring proper formatting
@ -69,6 +70,22 @@ async fn proxy_request(
let original_path = req.uri().path();
let path = get_destination_path(original_path, &config.prefix);
// Verify Host header
if let Some(host) = req.headers().get(hyper::header::HOST) {
let host_str = host.to_str().unwrap_or("");
if !is_valid_host(host_str, &config.trusted_hosts) {
return Ok(Response::builder()
.status(StatusCode::FORBIDDEN)
.body(Body::from("Invalid host header"))
.unwrap());
}
} else {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::from("Missing host header"))
.unwrap());
}
// Block access to /configs endpoint
if path.contains("/configs") {
return Ok(Response::builder()
@ -129,12 +146,29 @@ async fn proxy_request(
}
}
// Validates if the host header is allowed
fn is_valid_host(host: &str, trusted_hosts: &[String]) -> bool {
if host.is_empty() {
return false;
}
let host_without_port = if host.starts_with('[') { host.split(']').next().unwrap_or(host).trim_start_matches('[') } else { host.split(':').next().unwrap_or(host) };
let default_valid_hosts = ["localhost", "127.0.0.1"];
if default_valid_hosts.iter().any(|&valid| host_without_port.to_lowercase() == valid.to_lowercase()) {
return true;
}
trusted_hosts.iter().any(|valid| host_without_port.to_lowercase() == valid.to_lowercase())
}
/// Starts the proxy server
pub async fn start_server(
host: String,
port: u16,
prefix: String,
auth_token: String,
trusted_hosts: Vec<String>,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
// Check if server is already running
let mut handle_guard = SERVER_HANDLE.lock().await;
@ -152,6 +186,7 @@ pub async fn start_server(
upstream: "http://127.0.0.1:39291".to_string(),
prefix,
auth_token,
trusted_hosts,
};
// Create HTTP client

View File

@ -0,0 +1,43 @@
import { Input } from '@/components/ui/input'
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
import { useState, useEffect } from 'react'
export function TrustedHostsInput() {
const { trustedHosts, setTrustedHosts } = useLocalApiServer()
const [inputValue, setInputValue] = useState(trustedHosts.join(', '))
// Update input value when trustedHosts changes externally
useEffect(() => {
setInputValue(trustedHosts.join(', '))
}, [trustedHosts])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInputValue(value)
}
const handleBlur = () => {
// Split by comma and clean up each host
const hosts = inputValue
.split(',')
.map((host) => host.trim())
.filter((host) => host.length > 0)
// Remove duplicates
const uniqueHosts = [...new Set(hosts)]
setTrustedHosts(uniqueHosts)
setInputValue(uniqueHosts.join(', '))
}
return (
<Input
type="text"
value={inputValue}
onChange={handleChange}
onBlur={handleBlur}
className="w-full h-8 text-sm"
placeholder="Enter trusted hosts"
/>
)
}

View File

@ -21,6 +21,11 @@ type LocalApiServerState = {
// Verbose server logs
verboseLogs: boolean
setVerboseLogs: (value: boolean) => void
// Trusted hosts
trustedHosts: string[]
addTrustedHost: (host: string) => void
removeTrustedHost: (host: string) => void
setTrustedHosts: (hosts: string[]) => void
}
export const useLocalApiServer = create<LocalApiServerState>()(
@ -38,6 +43,16 @@ export const useLocalApiServer = create<LocalApiServerState>()(
setCorsEnabled: (value) => set({ corsEnabled: value }),
verboseLogs: true,
setVerboseLogs: (value) => set({ verboseLogs: value }),
trustedHosts: [],
addTrustedHost: (host) =>
set((state) => ({
trustedHosts: [...state.trustedHosts, host],
})),
removeTrustedHost: (host) =>
set((state) => ({
trustedHosts: state.trustedHosts.filter((h) => h !== host),
})),
setTrustedHosts: (hosts) => set({ trustedHosts: hosts }),
}),
{
name: localStorageKey.settingLocalApiServer,

View File

@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
import { ServerHostSwitcher } from '@/containers/ServerHostSwitcher'
import { PortInput } from '@/containers/PortInput'
import { ApiPrefixInput } from '@/containers/ApiPrefixInput'
import { TrustedHostsInput } from '@/containers/TrustedHostsInput'
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useAppState } from '@/hooks/useAppState'
@ -31,6 +32,7 @@ function LocalAPIServer() {
serverHost,
serverPort,
apiPrefix,
trustedHosts,
} = useLocalApiServer()
const { serverStatus, setServerStatus } = useAppState()
@ -43,6 +45,7 @@ function LocalAPIServer() {
host: serverHost,
port: serverPort,
prefix: apiPrefix,
trustedHosts,
isCorsEnabled: corsEnabled,
isVerboseEnabled: verboseLogs,
})
@ -179,6 +182,14 @@ function LocalAPIServer() {
)}
actions={<ApiPrefixInput />}
/>
<CardItem
title="Trusted Hosts"
description="Add trusted hosts that can access the API server"
className={cn(
isServerRunning && 'opacity-50 pointer-events-none'
)}
actions={<TrustedHostsInput />}
/>
</Card>
{/* Advanced Settings */}