Repository: static-web-server/static-web-server (language: rust)

Issue:
Add trusted_proxies option
### Search for duplicate feature request

- [X] I already searched, and this feature request or improvement is not a duplicate.

### Feature scope

Configuration (e.g. TOML) or CLI/env option

### Feature request related to a problem

The `log_remote_address` option currently always logs the contents of the `X-Forwarded-For` header. This is problematic when static-web-server is used without a trusted proxy in front, because a client could send this header and "poison" the log. More details can be found [in this MDN document](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns).

### Describe the solution you'd like

I would like a `trusted_proxies` option which would allow specifying from which IP addresses the `X-Forwarded-For` header is allowed to be read. I have actually already implemented this in https://github.com/static-web-server/static-web-server/commit/4cfa56978cda6b3bb3cbf88d8a2b72499db55c0f.

### Describe alternatives you've considered

An alternative would be to have a boolean `trust_x_forwarded`, which toggles the option for all IPs. This is not as fine grained though.

### Build target

All targets

### Additional context

My original Plan was to just submit a draft PR with the changes i made. Before doing that i looked for a Contributing.md file. When opening the PR i was a bit surprised to see the policy that PRs will only be accepted with a related issue, so it would be awesome if a contributing.md file is created :)


Discussion / hints:
The idea sounds fine to me. Feel free to open a draft.

Yes, we need a `CONTRIBUTING.md` file. I will create one.

Fix patch:
diff --git a/docs/content/configuration/command-line-arguments.md b/docs/content/configuration/command-line-arguments.md
index dffd5b5d..515dfd40 100644
--- a/docs/content/configuration/command-line-arguments.md
+++ b/docs/content/configuration/command-line-arguments.md
@@ -85,6 +85,10 @@ Options:
           Server TOML configuration file path [env: SERVER_CONFIG_FILE=] [default: ./config.toml]
       --log-remote-address [<LOG_REMOTE_ADDRESS>]
           Log incoming requests information along with its remote address if available using the `info` log level [env: SERVER_LOG_REMOTE_ADDRESS=] [default: false] [possible values: true, false]
+      --log-forwarded-for [<LOG_FORWARDED_FOR>]
+          Log real IP from X-Forwarded-For header [env: SERVER_LOG_FORWARDED_FOR] [default: false] [possible values: true, false]
+      --trusted-proxies <TRUSTED_PROXIES>
+          A comma separated list of IP addresses to accept the X-Forwarded-For header from. Empty means trust all IPs [env: SERVER_TRUSTED_PROXIES] [default: ""]
       --redirect-trailing-slash [<REDIRECT_TRAILING_SLASH>]
           Check for a trailing slash in the requested directory URI and redirect permanently (308) to the same path with a trailing slash suffix if it is missing [env: SERVER_REDIRECT_TRAILING_SLASH=] [default: true] [possible values: true, false]
       --ignore-hidden-files [<IGNORE_HIDDEN_FILES>]
diff --git a/docs/content/configuration/config-file.md b/docs/content/configuration/config-file.md
index 78a13597..d6058962 100644
--- a/docs/content/configuration/config-file.md
+++ b/docs/content/configuration/config-file.md
@@ -75,6 +75,12 @@ grace-period = 0
 #### Log request Remote Address if available
 log-remote-address = false
 
+#### Log real IP from X-Forwarded-For header if available
+log-forwarded-for = false
+
+#### IPs to accept the X-Forwarded-For header from. Empty means all
+trusted-proxies = []
+
 #### Redirect to trailing slash in the requested directory uri
 redirect-trailing-slash = true
 
diff --git a/docs/content/configuration/environment-variables.md b/docs/content/configuration/environment-variables.md
index 046bbf49..605fa080 100644
--- a/docs/content/configuration/environment-variables.md
+++ b/docs/content/configuration/environment-variables.md
@@ -30,6 +30,12 @@ Specify a logging level in lowercase. Possible values are `error`, `warn`, `info
 ### SERVER_LOG_REMOTE_ADDRESS
 Log incoming request information along with its Remote Address (IP) if available using the `info` log level. Default `false`.
 
+### SERVER_LOG_FORWARDED_FOR
+Log real IP from X-Forwarded-For header if available using the `info` log level. Default `false`
+
+### SERVER_TRUSTED_PROXIES
+A comma separated list of IP addresses to accept the X-Forwarded-For header from. An empty string means trust all IPs. Default `""`
+
 ### SERVER_ERROR_PAGE_404
 HTML file path for 404 errors. If the path is not specified or simply doesn't exist then the server will use a generic HTML error message.
 If a relative path is used then it will be resolved under the root directory. Default `./404.html`.
diff --git a/docs/content/features/logging.md b/docs/content/features/logging.md
index 335688dd..dd100ab7 100644
--- a/docs/content/features/logging.md
+++ b/docs/content/features/logging.md
@@ -27,55 +27,85 @@ Log entry example:
 2022-05-23T22:24:50.519540Z  INFO static_web_server::handler: incoming request: method=GET uri=/ remote_addr=192.168.1.126:57625
 ```
 
-Below is an example of how to enable Remote Address (IP) logging. Note the last two entries.
+Below is an example of how to enable Remote Address (IP) logging.
 
 ```sh
 static-web-server -a "0.0.0.0" -p 8080 -d docker/public/ -g info --log-remote-address=true
-# 2022-05-23T22:24:44.523057Z  INFO static_web_server::logger: logging level: info
-# 2022-05-23T22:24:44.523856Z  INFO static_web_server::server: server bound to TCP socket 0.0.0.0:8080
-# 2022-05-23T22:24:44.523962Z  INFO static_web_server::server: runtime worker threads: 4
-# 2022-05-23T22:24:44.523989Z  INFO static_web_server::server: security headers: enabled=false
-# 2022-05-23T22:24:44.524006Z  INFO static_web_server::server: auto compression: enabled=true
-# 2022-05-23T22:24:44.524061Z  INFO static_web_server::server: directory listing: enabled=false
-# 2022-05-23T22:24:44.524097Z  INFO static_web_server::server: directory listing order code: 6
-# 2022-05-23T22:24:44.524133Z  INFO static_web_server::server: cache control headers: enabled=true
-# 2022-05-23T22:24:44.524191Z  INFO static_web_server::server: basic authentication: enabled=false
-# 2022-05-23T22:24:44.524210Z  INFO static_web_server::server: grace period before graceful shutdown: 0s
-# 2022-05-23T22:24:44.524527Z  INFO Server::start_server{addr_str="0.0.0.0:8080" threads=4}: static_web_server::server: close time.busy=0.00ns time.idle=10.6µs
-# 2022-05-23T22:24:44.524585Z  INFO static_web_server::server: listening on http://0.0.0.0:8080
-# 2022-05-23T22:24:44.524614Z  INFO static_web_server::server: press ctrl+c to shut down the server
-# 2022-05-23T22:24:50.519540Z  INFO static_web_server::handler: incoming request: method=GET uri=/ remote_addr=192.168.1.126:57625
-# 2022-05-23T22:25:26.516841Z  INFO static_web_server::handler: incoming request: method=GET uri=/favicon.ico remote_addr=192.168.1.126:57625
+```
+
+The relevant log output:
+```log
+INFO static_web_server::logger: logging level: info
+<...>
+INFO static_web_server::info: log requests with remote IP addresses: enabled=true
+<...>
+INFO static_web_server::handler: incoming request: method=GET uri=/ remote_addr=192.168.1.126:57625
+INFO static_web_server::handler: incoming request: method=GET uri=/favicon.ico remote_addr=192.168.1.126:57625
 ```
 ## Log Real Remote IP
 
-When used behind reverse proxy, reported `remote_addr` indicate proxy internal IP address and port, and not client real remote IP.
-Proxy server can be configured to provide [X-Forwarded-For header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For), containing comma-separated list of IP addresses, starting with *client real remote IP*, and all following intermediate proxies (if any).
+When used behind a reverse proxy the reported `remote_addr` indicates the proxies IP address and port, not the clients real IP.
+The Proxy server can be configured to provide the [X-Forwarded-For header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For), containing a comma-separated list of IP addresses, starting with the *real remote client IP*, and all following intermediate proxies (if any).
+
+
+To enable logging of the real remote IP, enable the `--log-forwarded-for` option or the equivalent [SERVER_LOG_FORWARDED_FOR](/docs/content/configuration/environment-variables.md#serverlogforwardedfor) env. By default this will log all requests which have a correctly formatted `X-Forwarded-For` header. 
+
+Since the content of the `X-Forwarded-For` header can be changed by all proxies in the chain, the remote IP address reported may not be trusted.
 
-When *Remote Address (IP) logging* [is enabled](#log-remote-addresses), and `X-Forwarded-For` header is present and correctly formatted, then log entries for requests will contain a `real_remote_ip` section with IP of remote client, **as reported by this header**. 
+To restrict the logging to only trusted proxy IPs, you can use the `--trusted-proxies` option, or the equivalent [SERVER_TRUSTED_PROXIES](/docs/content/configuration/environment-variables.md#servertrustedproxies) env. This should be a list of IPs, separated by commas. An empty list (the default) indicates that all IPs should be trusted.
+
+Command used for the following examples:
+```sh
+static-web-server -a "::" --log-forwarded-for=true --trusted-proxies="::1" -p 8080 -d docker/public/ -g info
+```
+
+Look for these lines in the log output:
+```log
+<...>
+INFO static_web_server::info: log level: info
+INFO static_web_server::info: log requests with remote IP addresses: enabled=false
+INFO static_web_server::info: log X-Forwarded-For real remote IP addresses: enabled=true
+INFO static_web_server::info: trusted IPs for X-Forwarded-For: [::1]
+<...>
+```
 
 We can simulate request as from behind reverse proxy with additional intermediate-proxy with following command:
 
 ```sh
-curl --header "X-Forwarded-For: 203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348" http://0.0.0.0:8080
+curl "http://[::1]:8080" --header "X-Forwarded-For: 203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348"
 ```
 
-Log entry for such case will look like:
+Log entry for this request will look like:
 
 ```log
-2022-05-23T22:24:50.519540Z  INFO static_web_server::handler: incoming request: method=GET uri=/ remote_addr=192.168.1.126:57625 real_remote_ip=203.0.113.195
+INFO static_web_server::handler: incoming request: method=GET uri=/ real_remote_ip=203.0.113.195
 ```
 
-**`SWS`** will parse `X-Forwarded-For` header, and if format of provided IP is invalid - it will be ignored to prevent log poisoning attacks. In such case `real_remote_ip` section will not be added.
+---
+
+If we send the request from `127.0.0.1` instead:
+```sh
+curl "http://127.0.0.1:8080" --header "X-Forwarded-For: 203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348"
+```
+
+we get the following log output:
+```log
+INFO static_web_server::handler: incoming request: method=GET uri=/
+```
+`127.0.0.1` is not in the `trusted_proxies`, so we dont get a `real_remote_address` in the log.
+
+Note the absence of the proxies remote address in these examples. If you want to log the remote address and the real remote address, you need to specify both `--log-remote-address` and `--log-forwarded-for`.
+
+---
+
+**`SWS`** will parse the `X-Forwarded-For` header and if the provided client IP is invalid, it will be ignored to prevent log poisoning attacks. In such cases the `real_remote_ip` section will not be added.
 
 Example from above, but with invalid header:
 
 ```sh
-curl --header "X-Forwarded-For: <iframe src=//malware.attack>" http://0.0.0.0:8080
+curl "http://[::1]:8080" --header "X-Forwarded-For: <iframe src=//malware.attack>"
 ```
 
 ```log
-2022-05-23T22:24:50.519540Z  INFO static_web_server::handler: incoming request: method=GET uri=/ remote_addr=192.168.1.126:57625
+2022-05-23T22:24:50.519540Z  INFO static_web_server::handler: incoming request: method=GET uri=/
 ```
-
-Be aware, that contents of `X-Forwarded-For` header can be augumented by all proxies in the chain, and as such - remote IP address reported by it may not be trusted.
\ No newline at end of file
diff --git a/src/handler.rs b/src/handler.rs
index 7e0216be..79b1e09b 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -7,7 +7,12 @@
 //!
 
 use hyper::{Body, Request, Response, StatusCode};
-use std::{future::Future, net::SocketAddr, path::PathBuf, sync::Arc};
+use std::{
+    future::Future,
+    net::{IpAddr, SocketAddr},
+    path::PathBuf,
+    sync::Arc,
+};
 
 #[cfg(any(
     feature = "compression",
@@ -97,6 +102,10 @@ pub struct RequestHandlerOpts {
     pub index_files: Vec<String>,
     /// Log remote address feature.
     pub log_remote_address: bool,
+    /// Log the X-Forwarded-For header.
+    pub log_forwarded_for: bool,
+    /// Trusted IPs for remote addresses.
+    pub trusted_proxies: Vec<IpAddr>,
     /// Redirect trailing slash feature.
     pub redirect_trailing_slash: bool,
     /// Ignore hidden files feature.
@@ -152,6 +161,8 @@ impl Default for RequestHandlerOpts {
             basic_auth: String::new(),
             index_files: vec!["index.html".into()],
             log_remote_address: false,
+            log_forwarded_for: false,
+            trusted_proxies: Vec::new(),
             redirect_trailing_slash: true,
             ignore_hidden_files: false,
             disable_symlinks: false,
diff --git a/src/log_addr.rs b/src/log_addr.rs
index c0a98e14..27174c92 100644
--- a/src/log_addr.rs
+++ b/src/log_addr.rs
@@ -14,7 +14,18 @@ use crate::{handler::RequestHandlerOpts, health};
 /// Initializes the log address module.
 pub(crate) fn init(enabled: bool, handler_opts: &mut RequestHandlerOpts) {
     handler_opts.log_remote_address = enabled;
-    server_info!("log requests with remote and real IP addresses: enabled={enabled}");
+    let trusted = if handler_opts.trusted_proxies.is_empty() {
+        "all".to_owned()
+    } else {
+        format!("{:?}", handler_opts.trusted_proxies)
+    };
+
+    server_info!("log requests with remote IP addresses: enabled={enabled}");
+    server_info!(
+        "log X-Forwarded-For real remote IP addresses: enabled={}",
+        handler_opts.log_forwarded_for
+    );
+    server_info!("trusted IPs for X-Forwarded-For: {trusted}");
 }
 
 /// It logs remote and real IP addresses if available.
@@ -23,23 +34,27 @@ pub(crate) fn pre_process<T>(
     req: &Request<T>,
     remote_addr: Option<SocketAddr>,
 ) {
-    let remote_addrs = if opts.log_remote_address {
-        // Add a Remote IP if available
-        let remote_addr = remote_addr.map_or("".to_owned(), |ip| format!(" remote_addr={ip}"));
+    let mut remote_addrs = String::new();
 
-        // Add also a Real Remote IP if available
-        let real_remote_addr = req
+    if opts.log_remote_address {
+        if let Some(addr) = remote_addr {
+            remote_addrs.push_str(format!(" remote_addr={addr}").as_str());
+        }
+    }
+    if opts.log_forwarded_for
+        && (opts.trusted_proxies.is_empty()
+            || remote_addr.is_some_and(|addr| opts.trusted_proxies.contains(&addr.ip())))
+    {
+        if let Some(real_ip) = req
             .headers()
             .get("X-Forwarded-For")
             .and_then(|h| h.to_str().ok())
             .and_then(|s| s.split(',').next())
             .and_then(|s| s.trim().parse::<IpAddr>().ok())
-            .map_or("".to_owned(), |ip| format!(" real_remote_ip={ip}"));
-
-        [remote_addr, real_remote_addr].concat()
-    } else {
-        String::new()
-    };
+        {
+            remote_addrs.push_str(format!(" real_remote_ip={real_ip}").as_str());
+        }
+    }
 
     // Log incoming requests in debug mode only if the health option is enabled
     if opts.health && health::is_health_endpoint(req) {
diff --git a/src/server.rs b/src/server.rs
index 8df52d3b..209375ee 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -225,6 +225,12 @@ impl Server {
         // Log remote address option
         let log_remote_address = general.log_remote_address;
 
+        // Log the X-Forwarded-For header.
+        let log_forwarded_for = general.log_forwarded_for;
+
+        // Trusted IPs for remote addresses.
+        let trusted_proxies = general.trusted_proxies;
+
         // Log redirect trailing slash option
         let redirect_trailing_slash = general.redirect_trailing_slash;
         server_info!(
@@ -261,6 +267,8 @@ impl Server {
             page404: page404.clone(),
             page50x: page50x.clone(),
             log_remote_address,
+            log_forwarded_for,
+            trusted_proxies,
             redirect_trailing_slash,
             ignore_hidden_files,
             disable_symlinks,
diff --git a/src/settings/cli.rs b/src/settings/cli.rs
index 12b36106..80cc9df3 100644
--- a/src/settings/cli.rs
+++ b/src/settings/cli.rs
@@ -7,7 +7,7 @@
 
 use clap::Parser;
 use hyper::StatusCode;
-use std::path::PathBuf;
+use std::{net::IpAddr, path::PathBuf};
 
 #[cfg(feature = "directory-listing")]
 use crate::directory_listing::DirListFmt;
@@ -414,6 +414,28 @@ pub struct General {
     /// Log incoming requests information along with its remote address if available using the `info` log level.
     pub log_remote_address: bool,
 
+    #[arg(
+        long,
+        default_value = "false",
+        default_missing_value("true"),
+        num_args(0..=1),
+        require_equals(false),
+        action = clap::ArgAction::Set,
+        env = "SERVER_LOG_FORWARDED_FOR",
+    )]
+    /// Log the X-Forwarded-For header for remote IP information
+    pub log_forwarded_for: bool,
+
+    #[arg(
+        long,
+        require_equals(false),
+        value_delimiter(','),
+        action = clap::ArgAction::Set,
+        env = "SERVER_TRUSTED_PROXIES",
+    )]
+    /// List of IPs to use X-Forwarded-For from. The default is to trust all
+    pub trusted_proxies: Vec<IpAddr>,
+
     #[arg(
         long,
         default_value = "true",
diff --git a/src/settings/file.rs b/src/settings/file.rs
index cdd45994..98f0154e 100644
--- a/src/settings/file.rs
+++ b/src/settings/file.rs
@@ -8,6 +8,7 @@
 use headers::HeaderMap;
 use serde::Deserialize;
 use serde_repr::{Deserialize_repr, Serialize_repr};
+use std::net::IpAddr;
 use std::path::Path;
 use std::{collections::BTreeSet, path::PathBuf};
 
@@ -356,6 +357,12 @@ pub struct General {
     /// Log remote address feature.
     pub log_remote_address: Option<bool>,
 
+    /// Log the X-Forwarded-For header.
+    pub log_forwarded_for: Option<bool>,
+
+    /// Trusted IPs for remote addresses.
+    pub trusted_proxies: Option<Vec<IpAddr>>,
+
     /// Redirect trailing slash feature.
     pub redirect_trailing_slash: Option<bool>,
 
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index 5cd58b54..46a0f67f 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -197,6 +197,8 @@ impl Settings {
         let mut page_fallback = opts.page_fallback;
 
         let mut log_remote_address = opts.log_remote_address;
+        let mut log_forwarded_for = opts.log_forwarded_for;
+        let mut trusted_proxies = opts.trusted_proxies;
         let mut redirect_trailing_slash = opts.redirect_trailing_slash;
         let mut ignore_hidden_files = opts.ignore_hidden_files;
         let mut disable_symlinks = opts.disable_symlinks;
@@ -363,6 +365,12 @@ impl Settings {
                 if let Some(v) = general.log_remote_address {
                     log_remote_address = v
                 }
+                if let Some(v) = general.log_forwarded_for {
+                    log_forwarded_for = v
+                }
+                if let Some(v) = general.trusted_proxies {
+                    trusted_proxies = v
+                }
                 if let Some(v) = general.redirect_trailing_slash {
                     redirect_trailing_slash = v
                 }
@@ -651,6 +659,8 @@ impl Settings {
                 #[cfg(feature = "fallback-page")]
                 page_fallback,
                 log_remote_address,
+                log_forwarded_for,
+                trusted_proxies,
                 redirect_trailing_slash,
                 ignore_hidden_files,
                 disable_symlinks,


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
index bac712a7..77b2751c 100644
--- a/src/testing.rs
+++ b/src/testing.rs
@@ -94,6 +94,8 @@ pub mod fixtures {
             #[cfg(feature = "basic-auth")]
             basic_auth: general.basic_auth,
             log_remote_address: general.log_remote_address,
+            log_forwarded_for: general.log_forwarded_for,
+            trusted_proxies: general.trusted_proxies,
             redirect_trailing_slash: general.redirect_trailing_slash,
             ignore_hidden_files: general.ignore_hidden_files,
             disable_symlinks: general.disable_symlinks,