diff --git a/crates/turborepo-lib/src/task_hash.rs b/crates/turborepo-lib/src/task_hash.rs
index 8405b7554f896..a8c1a0f16b8b2 100644
--- a/crates/turborepo-lib/src/task_hash.rs
+++ b/crates/turborepo-lib/src/task_hash.rs
@@ -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",
diff --git a/crates/turborepo-microfrontends/src/error.rs b/crates/turborepo-microfrontends/src/error.rs
index 39e5990e0ee99..b34aca11b4b74 100644
--- a/crates/turborepo-microfrontends/src/error.rs
+++ b/crates/turborepo-microfrontends/src/error.rs
@@ -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 {
diff --git a/crates/turborepo-microfrontends/src/lib.rs b/crates/turborepo-microfrontends/src/lib.rs
index ba2536395f75b..f1404fdba68cb 100644
--- a/crates/turborepo-microfrontends/src/lib.rs
+++ b/crates/turborepo-microfrontends/src/lib.rs
@@ -26,8 +26,6 @@ mod configv1;
mod error;
mod schema;
-use std::io;
-
use configv1::ConfigV1;
pub use configv1::PathGroup;
pub use error::Error;
@@ -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
@@ -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