mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-24 20:52:11 +00:00
Added path validation to the share file endpoint
This commit is contained in:
parent
e64c4f01ec
commit
76b7c588de
@ -11,9 +11,20 @@ public sealed class PluginShareService(NativeShareService nativeShareService, Ru
|
|||||||
|
|
||||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginShareService).Namespace, nameof(PluginShareService));
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginShareService).Namespace, nameof(PluginShareService));
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Keep in sync with SHARE_FILE_EXTENSION in runtime/src/share_sheet.rs: the runtime only hands
|
||||||
|
/// archives with this extension to the native share sheet.
|
||||||
|
/// </remarks>
|
||||||
public const string PLUGIN_FILE_EXTENSION = ".mwplugin";
|
public const string PLUGIN_FILE_EXTENSION = ".mwplugin";
|
||||||
|
|
||||||
private const string PLUGIN_FILE_NAME = "plugin.lua";
|
private const string PLUGIN_FILE_NAME = "plugin.lua";
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Keep in sync with SHARE_DIRECTORY_NAME in runtime/src/share_sheet.rs: the runtime only hands
|
||||||
|
/// archives from a directory with this name to the native share sheet.
|
||||||
|
/// </remarks>
|
||||||
private const string TEMPORARY_ARCHIVE_DIRECTORY = "mindwork-ai-studio-plugin-shares";
|
private const string TEMPORARY_ARCHIVE_DIRECTORY = "mindwork-ai-studio-plugin-shares";
|
||||||
|
|
||||||
private const int TEMPORARY_ARCHIVE_RETENTION_HOURS = 24;
|
private const int TEMPORARY_ARCHIVE_RETENTION_HOURS = 24;
|
||||||
private const int FILE_NAME_PREFIX_MAX_LEN = 80;
|
private const int FILE_NAME_PREFIX_MAX_LEN = 80;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
use axum::Json;
|
use axum::Json;
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use crate::api_token::APIToken;
|
use crate::api_token::APIToken;
|
||||||
|
|
||||||
|
/// The directory the app creates its shareable plugin archives in. Keep in sync with
|
||||||
|
/// PluginShareService.TEMPORARY_ARCHIVE_DIRECTORY on the .NET side.
|
||||||
|
const SHARE_DIRECTORY_NAME: &str = "mindwork-ai-studio-plugin-shares";
|
||||||
|
|
||||||
|
/// The file extension of plugin archives, without the leading dot. Keep in sync with
|
||||||
|
/// PluginShareService.PLUGIN_FILE_EXTENSION on the .NET side.
|
||||||
|
const SHARE_FILE_EXTENSION: &str = "mwplugin";
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ShareFileRequest {
|
pub struct ShareFileRequest {
|
||||||
file_path: String,
|
file_path: String,
|
||||||
@ -25,6 +33,18 @@ pub async fn share_file(_token: APIToken, Json(request): Json<ShareFileRequest>)
|
|||||||
return failure(format!("The requested path is not an existing file: {}", path.to_string_lossy()));
|
return failure(format!("The requested path is not an existing file: {}", path.to_string_lossy()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve the path before validating it, so a symlink with a matching name cannot point at an
|
||||||
|
// arbitrary file. We share the original path afterwards, though: on Windows, canonicalize
|
||||||
|
// returns a \\?\ path, which the WinRT storage APIs do not accept.
|
||||||
|
let resolved_path = match std::fs::canonicalize(&path) {
|
||||||
|
Ok(resolved_path) => resolved_path,
|
||||||
|
Err(error) => return failure(format!("The requested path could not be resolved: {error}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_shareable_archive(&resolved_path) {
|
||||||
|
return failure(format!("The requested path is not a plugin archive created by AI Studio: {}", path.to_string_lossy()));
|
||||||
|
}
|
||||||
|
|
||||||
let result = share_file_on_platform(path).await;
|
let result = share_file_on_platform(path).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
@ -42,6 +62,22 @@ pub async fn share_file(_token: APIToken, Json(request): Json<ShareFileRequest>)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks that a path points to a plugin archive the app itself created for sharing. This keeps the
|
||||||
|
/// endpoint from handing arbitrary readable files to the operating system's share UI.
|
||||||
|
///
|
||||||
|
/// We match the directory by name instead of comparing it against the temporary directory: Rust and
|
||||||
|
/// .NET do not have to agree on where that is, and a mismatch would break sharing entirely.
|
||||||
|
fn is_shareable_archive(path: &Path) -> bool {
|
||||||
|
let has_archive_extension = path.extension().is_some_and(|extension| extension.eq_ignore_ascii_case(SHARE_FILE_EXTENSION));
|
||||||
|
if !has_archive_extension {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.parent()
|
||||||
|
.and_then(|parent| parent.file_name())
|
||||||
|
.is_some_and(|directory_name| directory_name == SHARE_DIRECTORY_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
fn failure(issue: impl Into<String>) -> Json<ShareFileResponse> {
|
fn failure(issue: impl Into<String>) -> Json<ShareFileResponse> {
|
||||||
Json(ShareFileResponse {
|
Json(ShareFileResponse {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user