这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
1 change: 1 addition & 0 deletions crates/turborepo-lib/src/task_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ impl<'a> TaskHasher<'a> {
"NEXT_*",
"USE_OUTPUT_FOR_EDGE_FUNCTIONS",
"NOW_BUILDER",
"VC_MICROFRONTENDS_CONFIG_FILE_NAME",
// Command Prompt casing of env variables
"APPDATA",
"PATH",
Expand Down
5 changes: 5 additions & 0 deletions crates/turborepo-microfrontends/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ pub enum Error {
},
#[error("Invalid package path: {0}. Path traversal outside repository root is not allowed.")]
PathTraversal(String),
#[error(
"Invalid custom config file name: {0}. Must be a .json or .jsonc file directly in the \
package root (no subdirectories or path traversal)."
)]
InvalidCustomConfigPath(String),
}

impl Error {
Expand Down
125 changes: 109 additions & 16 deletions crates/turborepo-microfrontends/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ mod configv1;
mod error;
mod schema;

use std::io;

use configv1::ConfigV1;
pub use configv1::PathGroup;
pub use error::Error;
Expand All @@ -43,6 +41,7 @@ pub const DEFAULT_MICROFRONTENDS_CONFIG_V1: &str = "microfrontends.json";
pub const DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT: &str = "microfrontends.jsonc";
pub const MICROFRONTENDS_PACKAGE: &str = "@vercel/microfrontends";
pub const SUPPORTED_VERSIONS: &[&str] = ["1"].as_slice();
pub const CUSTOM_CONFIG_ENV_VAR: &str = "VC_MICROFRONTENDS_CONFIG_FILE_NAME";

/// Strict Turborepo-only configuration for the microfrontends proxy.
/// This configuration parser only accepts fields that Turborepo's native proxy
Expand All @@ -57,6 +56,60 @@ pub struct TurborepoMfeConfig {
}

impl TurborepoMfeConfig {
/// Validates a custom config filename from environment variable.
/// Returns error if:
/// - Path contains ".." (attempting to traverse up)
/// - Path does not end with ".json" or ".jsonc"
/// - Path starts with "/" (must be relative)
/// - Path contains "/" or "\" (no subdirectories allowed)
fn validate_custom_config_name(filename: &str) -> Result<(), Error> {
// Must end with .json or .jsonc
if !filename.ends_with(".json") && !filename.ends_with(".jsonc") {
return Err(Error::InvalidCustomConfigPath(format!(
"{filename}: must be a JSON file ending with .json or .jsonc"
)));
}

// Must not contain directory separators (no subdirectories)
if filename.contains('/') || filename.contains('\\') {
return Err(Error::InvalidCustomConfigPath(format!(
"{filename}: subdirectories not allowed, file must be in package root"
)));
}

// Must not contain path traversal
if filename.contains("..") {
return Err(Error::InvalidCustomConfigPath(format!(
"{filename}: path traversal not allowed"
)));
}

// Must be relative (not start with /)
if filename.starts_with('/') {
return Err(Error::InvalidCustomConfigPath(format!(
"{filename}: must be relative to package root"
)));
}

Ok(())
}

/// Gets the custom config filename from environment variable if set.
/// Returns None if not set, or Error if invalid.
fn get_custom_config_name() -> Result<Option<String>, Error> {
match std::env::var(CUSTOM_CONFIG_ENV_VAR) {
Ok(filename) if !filename.is_empty() => {
Self::validate_custom_config_name(&filename)?;
Ok(Some(filename))
}
Ok(_) => Ok(None), // Empty string means not set
Err(std::env::VarError::NotPresent) => Ok(None),
Err(std::env::VarError::NotUnicode(_)) => Err(Error::InvalidCustomConfigPath(
"environment variable contains invalid UTF-8".to_string(),
)),
}
}

/// Reads config from given path using strict Turborepo schema.
/// Returns `Ok(None)` if the file does not exist
pub fn load(config_path: &AbsoluteSystemPath) -> Result<Option<Self>, Error> {
Expand Down Expand Up @@ -89,10 +142,9 @@ impl TurborepoMfeConfig {

Config::validate_package_path(repo_root, &absolute_dir)?;

let Some((contents, path)) = Self::load_v1_dir(&absolute_dir) else {
let Some((contents, path)) = Self::load_v1_dir(&absolute_dir)? else {
return Ok(None);
};
let contents = contents?;
let mut config = Self::from_str_with_mfe_dep(&contents, path.as_str(), has_mfe_dependency)?;
config.filename = path
.file_name()
Expand Down Expand Up @@ -200,15 +252,36 @@ impl TurborepoMfeConfig {

fn load_v1_dir(
dir: &AbsoluteSystemPath,
) -> Option<(Result<String, io::Error>, AbsoluteSystemPathBuf)> {
) -> Result<Option<(String, AbsoluteSystemPathBuf)>, Error> {
let load_config =
|filename: &str| -> Option<(Result<String, io::Error>, AbsoluteSystemPathBuf)> {
|filename: &str| -> Option<(Result<String, Error>, AbsoluteSystemPathBuf)> {
let path = dir.join_component(filename);
let contents = path.read_existing_to_string().transpose()?;
let contents = path
.read_existing_to_string()
.transpose()?
.map_err(Error::from);
Some((contents, path))
};
load_config(DEFAULT_MICROFRONTENDS_CONFIG_V1)
.or_else(|| load_config(DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT))

// First check if custom config is specified via environment variable
match Self::get_custom_config_name()? {
Some(custom_name) => {
// If environment variable is set, only try that path
let Some((contents, path)) = load_config(&custom_name) else {
return Ok(None);
};
Ok(Some((contents?, path)))
}
None => {
// Otherwise use default config file names
let Some((contents, path)) = load_config(DEFAULT_MICROFRONTENDS_CONFIG_V1)
.or_else(|| load_config(DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT))
else {
return Ok(None);
};
Ok(Some((contents?, path)))
}
}
}

pub fn set_path(&mut self, dir: &AnchoredSystemPath) {
Expand Down Expand Up @@ -300,10 +373,9 @@ impl Config {
Self::validate_package_path(repo_root, &absolute_dir)?;

// we want to try different paths and then do `from_str`
let Some((contents, path)) = Self::load_v1_dir(&absolute_dir) else {
let Some((contents, path)) = Self::load_v1_dir(&absolute_dir)? else {
return Ok(None);
};
let contents = contents?;
let mut config = Config::from_str(&contents, path.as_str())?;
config.filename = path
.file_name()
Expand Down Expand Up @@ -390,15 +462,36 @@ impl Config {

fn load_v1_dir(
dir: &AbsoluteSystemPath,
) -> Option<(Result<String, io::Error>, AbsoluteSystemPathBuf)> {
) -> Result<Option<(String, AbsoluteSystemPathBuf)>, Error> {
let load_config =
|filename: &str| -> Option<(Result<String, io::Error>, AbsoluteSystemPathBuf)> {
|filename: &str| -> Option<(Result<String, Error>, AbsoluteSystemPathBuf)> {
let path = dir.join_component(filename);
let contents = path.read_existing_to_string().transpose()?;
let contents = path
.read_existing_to_string()
.transpose()?
.map_err(Error::from);
Some((contents, path))
};
load_config(DEFAULT_MICROFRONTENDS_CONFIG_V1)
.or_else(|| load_config(DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT))

// First check if custom config is specified via environment variable
match TurborepoMfeConfig::get_custom_config_name()? {
Some(custom_name) => {
// If environment variable is set, only try that path
let Some((contents, path)) = load_config(&custom_name) else {
return Ok(None);
};
Ok(Some((contents?, path)))
}
None => {
// Otherwise use default config file names
let Some((contents, path)) = load_config(DEFAULT_MICROFRONTENDS_CONFIG_V1)
.or_else(|| load_config(DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT))
else {
return Ok(None);
};
Ok(Some((contents?, path)))
}
}
}

/// Sets the path the configuration was loaded from
Expand Down
Loading