security upgrade
This commit is contained in:
+77
-12
@@ -16,6 +16,7 @@ pub async fn upload_pad_file(
|
||||
Path(slug): Path<String>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
require_authenticated_upload(&state, &headers).await?;
|
||||
let mut password: Option<String> = None;
|
||||
let mut access_token: Option<String> = None;
|
||||
let mut file: Option<(String, Vec<u8>)> = None;
|
||||
@@ -55,8 +56,9 @@ pub async fn upload_pad_file(
|
||||
&state,
|
||||
&slug,
|
||||
password.as_deref(),
|
||||
access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let level = if db::verify_pad_password(&pad, password.as_deref())
|
||||
@@ -68,7 +70,7 @@ pub async fn upload_pad_file(
|
||||
&state,
|
||||
"pad",
|
||||
&slug,
|
||||
access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -144,8 +146,9 @@ pub async fn pad_files(
|
||||
&state,
|
||||
&slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let mut files = db::list_pad_files(&state.db, pad.id).await?;
|
||||
@@ -175,8 +178,9 @@ pub async fn delete_pad_file(
|
||||
&state,
|
||||
&slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
if !crate::auth::is_resource_owner(&state, "pad", &pad.slug, bearer_token(&headers))
|
||||
@@ -201,6 +205,7 @@ pub async fn upload_note_file(
|
||||
Path((workspace_slug, note_slug)): Path<(String, String)>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
require_authenticated_upload(&state, &headers).await?;
|
||||
let mut password: Option<String> = None;
|
||||
let mut access_token: Option<String> = None;
|
||||
let mut file: Option<(String, Vec<u8>)> = None;
|
||||
@@ -241,8 +246,9 @@ pub async fn upload_note_file(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
password.as_deref(),
|
||||
access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -255,7 +261,7 @@ pub async fn upload_note_file(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -309,8 +315,9 @@ pub async fn delete_note(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -323,7 +330,7 @@ pub async fn delete_note(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -360,8 +367,9 @@ pub async fn note_files(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let mut files = db::list_note_files(&state.db, note.id).await?;
|
||||
@@ -392,8 +400,9 @@ pub async fn delete_note_file(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
payload.access_token.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let workspace_owner = crate::auth::is_resource_owner(
|
||||
@@ -429,6 +438,19 @@ pub async fn delete_note_file(
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
async fn require_authenticated_upload(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<(), ApiError> {
|
||||
let user = crate::auth::optional_user(state, headers)
|
||||
.await
|
||||
.map_err(|error| ApiError::forbidden(&error.message))?;
|
||||
if user.is_none() {
|
||||
return Err(ApiError::forbidden("Log in to upload files."));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_file(
|
||||
State(state): State<SharedState>,
|
||||
Path((token, filename)): Path<(String, String)>,
|
||||
@@ -476,13 +498,29 @@ async fn serve_token_file(
|
||||
.get_local_with_legacy(&key, &legacy_key)
|
||||
.await
|
||||
.map_err(|_| ApiError::not_found_file())?;
|
||||
let mime = mime_guess::from_path(&safe).first_or_octet_stream();
|
||||
let guessed_mime = mime_guess::from_path(&safe).first_or_octet_stream();
|
||||
let inline_image = is_safe_inline_image_mime(guessed_mime.essence_str());
|
||||
let served_mime = if inline_image {
|
||||
guessed_mime.as_ref()
|
||||
} else {
|
||||
"application/octet-stream"
|
||||
};
|
||||
let disposition = if inline_image { "inline" } else { "attachment" };
|
||||
let mut response = bytes.into_response();
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime.as_ref())
|
||||
HeaderValue::from_str(served_mime)
|
||||
.unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_DISPOSITION,
|
||||
HeaderValue::from_str(&format!("{disposition}; filename=\"{safe}\""))
|
||||
.expect("sanitized attachment filename"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static("content-security-policy"),
|
||||
HeaderValue::from_static("default-src 'none'; sandbox"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::X_CONTENT_TYPE_OPTIONS,
|
||||
HeaderValue::from_static("nosniff"),
|
||||
@@ -501,6 +539,19 @@ async fn serve_token_file(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn is_safe_inline_image_mime(value: &str) -> bool {
|
||||
matches!(
|
||||
value,
|
||||
"image/png"
|
||||
| "image/jpeg"
|
||||
| "image/gif"
|
||||
| "image/webp"
|
||||
| "image/avif"
|
||||
| "image/bmp"
|
||||
| "image/x-icon"
|
||||
)
|
||||
}
|
||||
|
||||
fn sanitize_filename(value: &str) -> String {
|
||||
let name = std::path::Path::new(value)
|
||||
.file_name()
|
||||
@@ -522,3 +573,17 @@ fn sanitize_filename(value: &str) -> String {
|
||||
clean.chars().take(160).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_safe_inline_image_mime;
|
||||
|
||||
#[test]
|
||||
fn only_raster_images_are_inline() {
|
||||
assert!(is_safe_inline_image_mime("image/png"));
|
||||
assert!(is_safe_inline_image_mime("image/jpeg"));
|
||||
assert!(!is_safe_inline_image_mime("image/svg+xml"));
|
||||
assert!(!is_safe_inline_image_mime("text/html"));
|
||||
assert!(!is_safe_inline_image_mime("application/xml"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user