Repository: tokio-rs/axum
File: axum-extra/src/response/file_stream.rs (Rust)

Code before:
            if let Some(file_name_str) = file_name_os.to_str() {
                file_name = Some(file_name_str.to_owned());
            }
        }

        Ok(Self {
            stream: ReaderStream::new(file),
            file_name,
            content_size,
        })
    }
}

impl<S> IntoResponse for FileStream<S>
where
    S: TryStream + Send + 'static,
    S::Ok: Into<Bytes>,
    S::Error: Into<BoxError>,
{
    fn into_response(self) -> Response {
        let mut resp = Response::builder().header(header::CONTENT_TYPE, "application/octet-stream");

        if let Some(file_name) = self.file_name {
            // Escape backslashes and double quotes in the filename to prevent
            // Content-Disposition header parameter injection (similar to CVE-2023-29401)
            let escaped = file_name.replace('\\', "\\\\").replace('"', "\\\"");
            resp = resp.header(
                header::CONTENT_DISPOSITION,
                format!("attachment; filename=\"{escaped}\""),
            );
        }

        if let Some(content_size) = self.content_size {
            resp = resp.header(header::CONTENT_LENGTH, content_size);
        }

        resp.body(body::Body::from_stream(self.stream))
            .unwrap_or_else(|e| {
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("build FileStream response error: {e}"),
                )
                    .into_response()
            })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::{extract::Request, routing::get, Router};

Review comment (refactor):
Could we extract this into its own struct and do the escaping in the format method? That way, we could re-use this with https://github.com/tokio-rs/axum/pull/3664/changes#diff-440bd42710daa4aa996f51e5e4e6034870aa1a3d12660c99f36199ca7a78376aR93-R97 ?

Code after:
            if let Some(file_name_str) = file_name_os.to_str() {
                file_name = Some(file_name_str.to_owned());
            }
        }

        Ok(Self {
            stream: ReaderStream::new(file),
            file_name,
            content_size,
        })
    }
}

impl<S> IntoResponse for FileStream<S>
where
    S: TryStream + Send + 'static,
    S::Ok: Into<Bytes>,
    S::Error: Into<BoxError>,
{
    fn into_response(self) -> Response {
        let mut resp = Response::builder().header(header::CONTENT_TYPE, "application/octet-stream");

        if let Some(file_name) = self.file_name {
            resp = resp.header(
                header::CONTENT_DISPOSITION,
                format!(
                    "attachment; filename=\"{}\"",
                    super::content_disposition::EscapedFilename(&file_name)
                ),
            );
        }

        if let Some(content_size) = self.content_size {
            resp = resp.header(header::CONTENT_LENGTH, content_size);
        }

        resp.body(body::Body::from_stream(self.stream))
            .unwrap_or_else(|e| {
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("build FileStream response error: {e}"),
                )
                    .into_response()
            })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::{extract::Request, routing::get, Router};