Help about `axum`

I have a type:

use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct FileResp {
    pub content: String,
}

and a server:

use axum::response::{IntoResponse, Response};
use axum::{
    extract::Path, 
    response::Json, 
    routing::get, 
    Router
};
// use reqwest::StatusCode;
use axum::http::StatusCode;
use super::types::FileResp;

pub async fn server() {
    let app = Router::new().route("/files/:name", get(get_file));

    let listsener = tokio::net::TcpListener::bind("localhost:8081")
        .await
        .unwrap();

    axum::serve(listsener, app).await.unwrap();
}

async fn get_file(Path(name): Path<String>) -> Response {
    let file_name = format!("./files/{}", name);
    let content = std::fs::read_to_string(file_name).unwrap_or_default();
    FileResp { content }.into_response()
}

impl IntoResponse for FileResp {
    fn into_response(self) -> Response {
        let body = serde_json::to_string(&self).unwrap();
        Response::builder()
            .status(StatusCode::OK)
            .header("Content-Type", "application/json")
            .body(body.into())
            .unwrap()
    }
}

But the function get_file cannot return correct response:


As can be seen, the response has no Content-Type.
What am I doing wrong?

Is there a reason you don't use tower_http::services::ServeDir to serve the files? I don't think your get_file method is safe as the path can contain ../ segments, which would allow somebody to read arbitrary files from your server.

1 Like

Once you get your endpoint working, try calling it with variations on file=../etc/passwd, file=../../etc/passwd, and so on. You've written a fairly classic path traversal vulnerability, and someone can use this to read any file on the host that the server has read access to (which is a lot).

The standard fix is to normalize the path, which resolves any relative path segments, symbolic links, and so on, leaving only the final path to the target file, and then to check that that path is within the directory or directories you intend to provide access to. In a case like this, where you're serving everything that's in files/, you can use this hander to do the work, too.

You can return Json<T> to have Axum automatically handle the encoding and headers for you:

async fn get_file(Path(name): Path<String>) -> Json<FileResp> {
    let file_name = todo!();
    let content = std::fs::read_to_string(file_name).unwrap_or_default();
    Json(FileResp { content })
}

1 Like

That's ok, I'm doing this just for testing. Do you have any clue on this issue? I've spend two days on this, but get nothing.

I've tried to return Json, then the Content-Type became text/plain.


I can't reproduce that:

use axum::{extract::Path, response::Json, routing::get, Router};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct FileResp {
    pub content: String,
}

#[tokio::main]
async fn main() {
    server().await
}

pub async fn server() {
    let app = Router::new().route("/files/:name", get(get_file));

    let listsener = tokio::net::TcpListener::bind("localhost:8081")
        .await
        .unwrap();

    axum::serve(listsener, app).await.unwrap();
}

async fn get_file(Path(name): Path<String>) -> Json<FileResp> {
    Json(FileResp { content: name })
}
[package]
name = "baiguoname-hello-world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = "0.7.5"
serde = { version = "1.0.203", features = ["derive"] }
tokio = { version = "1.38.0", features = ["rt", "rt-multi-thread"] }
% curl -v http://localhost:8081/files/example
* Host localhost:8081 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8081...
* Connected to localhost (::1) port 8081
> GET /files/example HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 200 OK
< content-type: application/json
< content-length: 21
< date: Sun, 30 Jun 2024 15:22:09 GMT
< 
* Connection #0 to host localhost left intact
{"content":"example"}

Note the content-type header on the response, which is application/json as desired.

Can you post something runnable that demonstrates the behaviour?

2 Likes

I've made github url. You can run it as the README.md shows, and then in the web click the button "Add" like this:


And then click then button "Read File".

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.