Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
389 changes: 317 additions & 72 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "defguard-proxy"
version = "1.5.0"
version = "1.5.1"
edition = "2021"
license = "Apache-2.0"
homepage = "https://github.com/DefGuard/proxy"
Expand Down Expand Up @@ -51,6 +51,7 @@ mime_guess = "2.0"
base64 = "0.22"
tower = "0.5"
futures-util = "0.3"
ammonia = "4.1.1"

[build-dependencies]
tonic-prost-build = "0.14"
Expand Down
16 changes: 16 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Emitter::default().add_instructions(&git2)?.emit()?;

tonic_prost_build::configure()
// These types contain sensitive data.
.skip_debug([
"ActivateUserRequest",
"AuthInfoResponse",
"AuthenticateRequest",
"AuthenticateResponse",
"ClientMfaFinishResponse",
"CodeMfaSetupStartResponse",
"CodeMfaSetupFinishResponse",
"CoreRequest",
"CoreResponse",
"DeviceConfigResponse",
"InstanceInfoResponse",
"NewDevice",
"PasswordResetRequest",
])
// Enable optional fields.
.protoc_arg("--experimental_allow_proto3_optional")
// Make all messages serde-serializable.
Expand Down
5 changes: 4 additions & 1 deletion src/enterprise/handlers/desktop_client_mfa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ pub(super) async fn mfa_auth_callback(
info!("MFA authentication callback completed successfully");
Ok(private_cookies)
} else {
error!("Received invalid gRPC response type during handling the MFA OpenID authentication callback: {payload:#?}");
error!(
"Received invalid gRPC response type during handling the MFA OpenID authentication \
callback"
);
Err(ApiError::InvalidResponseType)
}
}
9 changes: 6 additions & 3 deletions src/enterprise/handlers/openid_login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async fn auth_info(
.send(core_request::Payload::AuthInfo(request), device_info)?;
let payload = get_core_response(rx).await?;
if let core_response::Payload::AuthInfo(response) = payload {
debug!("Received auth info {response:?}");
debug!("Received auth info response");

let nonce_cookie = Cookie::build((NONCE_COOKIE_NAME, response.nonce))
// .domain(cookie_domain)
Expand All @@ -117,7 +117,7 @@ async fn auth_info(
let auth_info = AuthInfo::new(response.url, response.button_display_name);
Ok((private_cookies, Json(auth_info)))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
Expand Down Expand Up @@ -188,7 +188,10 @@ async fn auth_callback(
debug!("Received auth callback response {url:?} {token:?}");
Ok((private_cookies, Json(CallbackResponseData { url, token })))
} else {
error!("Received invalid gRPC response type during handling the OpenID authentication callback: {payload:#?}");
error!(
"Received invalid gRPC response type during handling the OpenID authentication \
callback"
);
Err(ApiError::InvalidResponseType)
}
}
8 changes: 4 additions & 4 deletions src/grpc.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::{
any::Any,
collections::HashMap,
net::SocketAddr,
sync::{
Expand All @@ -21,7 +22,6 @@ use crate::{
// connected clients
type ClientMap = HashMap<SocketAddr, mpsc::UnboundedSender<Result<CoreRequest, Status>>>;

#[derive(Debug)]
pub(crate) struct ProxyServer {
current_id: Arc<AtomicU64>,
clients: Arc<Mutex<ClientMap>>,
Expand All @@ -45,7 +45,7 @@ impl ProxyServer {

/// Sends message to the other side of RPC, with given `payload` and optional `device_info`.
/// Returns `tokio::sync::oneshot::Reveicer` to let the caller await reply.
#[instrument(name = "send_grpc_message", level = "debug", skip(self))]
#[instrument(name = "send_grpc_message", level = "debug", skip(self, payload))]
pub(crate) fn send(
&self,
payload: core_request::Payload,
Expand Down Expand Up @@ -127,13 +127,13 @@ impl proxy_server::Proxy for ProxyServer {
loop {
match stream.message().await {
Ok(Some(response)) => {
debug!("Received message from Defguard core: {response:?}");
debug!("Received message from Defguard Core ID={}", response.id);
connected.store(true, Ordering::Relaxed);
// Discard empty payloads.
if let Some(payload) = response.payload {
if let Some(rx) = results.lock().unwrap().remove(&response.id) {
if let Err(err) = rx.send(payload) {
error!("Failed to send message to rx: {err:?}");
error!("Failed to send message to rx {:?}", err.type_id());
}
} else {
error!("Missing receiver for response #{}", response.id);
Expand Down
6 changes: 3 additions & 3 deletions src/handlers/desktop_client_mfa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ async fn start_client_mfa(
info!("Started desktop client authorization {req:?}");
Ok(Json(response))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
Expand All @@ -170,7 +170,7 @@ async fn finish_client_mfa(
if let core_response::Payload::ClientMfaFinish(response) = payload {
Ok(Json(response))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
Expand Down Expand Up @@ -210,7 +210,7 @@ async fn finish_remote_mfa(
Err(ApiError::Unexpected(String::new()))
}
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
12 changes: 6 additions & 6 deletions src/handlers/enrollment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ async fn start_enrollment_process(

Ok((private_cookies.add(cookie), Json(response)))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}

#[instrument(level = "debug", skip(state))]
#[instrument(level = "debug", skip(state, req))]
async fn activate_user(
State(state): State<AppState>,
device_info: DeviceInfo,
Expand Down Expand Up @@ -94,12 +94,12 @@ async fn activate_user(
}
Ok(private_cookies)
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}

#[instrument(level = "debug", skip(state))]
#[instrument(level = "debug", skip(state, req))]
async fn create_device(
State(state): State<AppState>,
device_info: DeviceInfo,
Expand All @@ -122,7 +122,7 @@ async fn create_device(
info!("Added new device {name} {pubkey}");
Ok(Json(response))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
Expand Down Expand Up @@ -150,7 +150,7 @@ async fn get_network_info(
info!("Got network info for device {pubkey}");
Ok(Json(response))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
2 changes: 1 addition & 1 deletion src/handlers/mobile_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub(crate) async fn register_mobile_auth(
info!("Registered mobile device for auth");
Ok(())
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
108 changes: 106 additions & 2 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ where
let user_agent = TypedHeader::<UserAgent>::from_request_parts(parts, state)
.await
.map(|v| v.to_string())
.ok();
.ok()
// sanitize user-agent
.filter(|agent| !ammonia::is_html(agent));

let ip_address = forwarded_for_ip
.or(insecure_ip)
Expand All @@ -55,7 +57,7 @@ where
pub(crate) async fn get_core_response(rx: Receiver<Payload>) -> Result<Payload, ApiError> {
debug!("Fetching core response...");
if let Ok(core_response) = timeout(CORE_RESPONSE_TIMEOUT, rx).await {
debug!("Got gRPC response from Defguard core: {core_response:?}");
debug!("Got gRPC response from Defguard Core");
if let Ok(Payload::CoreError(core_error)) = core_response {
if core_error.status_code == Code::FailedPrecondition as i32
&& core_error.message == "no valid license"
Expand All @@ -76,3 +78,105 @@ pub(crate) async fn get_core_response(rx: Receiver<Payload>) -> Result<Payload,
Err(ApiError::CoreTimeout)
}
}

#[cfg(test)]
mod tests {
use super::*;
use axum::{body::Body, http::Request};

static VALID_USER_AGENTS: &[&str] = &[
// desktop
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.1 43.03",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.3 21.05",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.3 17.34",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.3 3.72",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Trailer/93.3.8652.5 2.48",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0. 2.48",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0. 2.48",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0. 2.48",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0. 1.24",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.1958 1.24",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136. 1.24",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.3 1.24",

// mobile
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.3 63.11",
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Mobile/15E148 Safari/604. 8.25",
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/360.1.737798518 Mobile/15E148 Safari/604. 5.83",
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/134.0.6998.99 Mobile/15E148 Safari/604. 4.85",
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/125.0.0.0 Mobile Safari/537.3 3.88",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604. 3.4",
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Mobile/15E148 Safari/604. 1.94",
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1.1 Mobile/15E148 Safari/604. 1.94",
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.3 1.46",
"Mozilla/5.0 (Android 14; Mobile; rv:136.0) Gecko/136.0 Firefox/136. 0.97",
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Mobile Safari/537.3 0.97",
"Mozilla/5.0 (Linux; Android 10; JNY-LX1; HMSCore 6.15.0.302) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 HuaweiBrowser/15.0.4.312 Mobile Safari/537.3 0.97",
"Mozilla/5.0 (Android 15; Mobile; rv:136.0) Gecko/136.0 Firefox/136. 0.49",
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e YisouSpider/5.0 Safari/602. 0.49",
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604. 0.49",
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.3 0.49",
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.3 0.49",
];

static INVALID_USER_AGENTS: &[&str] = &[
"<h1><a href=\"//isec.pl\">CLICK HERE</a></h1>",
"<html><script>alert(\"test\")</script></html>",
"<h1><a href=\"//isec.pl\">CLICK HERE",
];

struct DummyState;

#[tokio::test]
async fn test_user_agent_sanitization_dg25_16() {
let state = DummyState;

// valid user agents
for agent in VALID_USER_AGENTS {
let req = Request::builder()
.header("User-Agent", *agent)
.header("X-Forwarded-For", "10.0.0.1")
.body(Body::empty())
.unwrap();
let (parts, _) = req.into_parts();
let mut parts = parts;

let device_info = DeviceInfo::from_request_parts(&mut parts, &state)
.await
.expect("should succeed");

assert_eq!(device_info.user_agent, Some(agent.to_string()));
}

// invalid user agents
for agent in INVALID_USER_AGENTS {
let req = Request::builder()
.header("User-Agent", *agent)
.header("X-Forwarded-For", "10.0.0.1")
.body(Body::empty())
.unwrap();
let (parts, _) = req.into_parts();
let mut parts = parts;

let device_info = DeviceInfo::from_request_parts(&mut parts, &state)
.await
.expect("should succeed");

assert!(device_info.user_agent.is_none());
}

// no user agent
let req = Request::builder()
.header("X-Forwarded-For", "10.0.0.1")
.body(Body::empty())
.unwrap();
let (parts, _) = req.into_parts();
let mut parts = parts;

let device_info = DeviceInfo::from_request_parts(&mut parts, &state)
.await
.expect("should succeed");

assert!(device_info.user_agent.is_none());
}
}
8 changes: 4 additions & 4 deletions src/handlers/password_reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async fn request_password_reset(
info!("Started password reset request for {}", req.email);
Ok(())
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
Expand Down Expand Up @@ -70,12 +70,12 @@ async fn start_password_reset(
info!("Started password reset process");
Ok((private_cookies.add(cookie), Json(response)))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}

#[instrument(level = "debug", skip(state))]
#[instrument(level = "debug", skip(state, req))]
async fn reset_password(
State(state): State<AppState>,
device_info: DeviceInfo,
Expand All @@ -100,7 +100,7 @@ async fn reset_password(
}
Ok(private_cookies)
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
2 changes: 1 addition & 1 deletion src/handlers/polling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub(crate) async fn info(
info!("Retrieved info for polling request");
Ok(Json(response))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type");
Err(ApiError::InvalidResponseType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import { FormInput } from '../../../../shared/components/Form/FormInput/FormInpu
import { Card } from '../../../../shared/components/layout/Card/Card';
import { MessageBoxOld } from '../../../../shared/components/layout/MessageBox/MessageBoxOld';
import { MessageBoxType } from '../../../../shared/components/layout/MessageBox/types';
import { patternValidPhoneNumber } from '../../../../shared/patterns';
import { EnrollmentStepIndicator } from '../../components/EnrollmentStepIndicator/EnrollmentStepIndicator';
import { useEnrollmentStore } from '../../hooks/store/useEnrollmentStore';

const phonePattern = /^\+?[0-9]+( [0-9]+)?$/;

export const DataVerificationStep = () => {
const { LL } = useI18nContext();
const submitRef = useRef<HTMLInputElement | null>(null);
Expand All @@ -38,7 +37,7 @@ export const DataVerificationStep = () => {
.trim()
.refine((val) => {
if (val && typeof val === 'string' && val.length > 0) {
return phonePattern.test(val);
return patternValidPhoneNumber.test(val);
}
return true;
}, LL.form.errors.invalid()),
Expand Down
Loading