Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 129 additions & 20 deletions src/Auth_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ private function create_auth( array $assoc_args, bool $global, string $site_url
if ( 'default' === $site_url ) {
$this->generate_global_auth_files();
} else {
$this->generate_site_auth_files( $site_url );
$this->generate_site_auth_files( $site_url, $this->site_data );
}

EE::log( 'Reloading global reverse proxy.' );
Expand Down Expand Up @@ -200,7 +200,7 @@ private function create_whitelist( string $site_url, string $ips ) {
if ( 'default' === $site_url ) {
$this->generate_global_whitelist();
} else {
$this->generate_site_whitelist( $site_url );
$this->generate_site_whitelist( $site_url, $this->site_data );
}

reload_global_nginx_proxy();
Expand Down Expand Up @@ -283,7 +283,10 @@ private function generate_global_auth_files() {
);

foreach ( $sites as $site ) {
$this->generate_site_auth_files( $site );
// Fetch site data to get app_sub_type and alias_domains
$site_info = \EE\Model\Site::where( 'site_url', $site );
$site_data = ! empty( $site_info ) ? $site_info[0] : null;
$this->generate_site_auth_files( $site, $site_data );
}
}
}
Expand All @@ -292,25 +295,77 @@ private function generate_global_auth_files() {
* Generates auth files for a site
*
* @param string $site_url URL of site
* @param object $site_data Optional site data object containing app_sub_type and alias_domains
*
* @throws Exception
*/
private function generate_site_auth_files( string $site_url ) {
$site_auth_file = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/' . $site_url;
$this->fs->remove( $site_auth_file );
private function generate_site_auth_files( string $site_url, $site_data = null ) {
// Always clean up wildcard file first (handles site type changes from subdom to regular)
$wildcard_file = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/_wildcard.' . $site_url;
$this->fs->remove( $wildcard_file );

// Collect all domains to generate htpasswd files for
$domains = [ $site_url ];

// For subdomain multisites, add wildcard file
if ( $site_data && ! empty( $site_data->app_sub_type ) && 'subdom' === $site_data->app_sub_type ) {
$domains[] = '_wildcard.' . $site_url;
}

// Add alias domains (excluding main site_url)
if ( $site_data && ! empty( $site_data->alias_domains ) ) {
$alias_list = array_map( 'trim', explode( ',', $site_data->alias_domains ) );
foreach ( $alias_list as $alias ) {
if ( empty( $alias ) || $alias === $site_url ) {
continue;
}
// Skip *.site_url as it transforms to _wildcard.site_url (already added for subdomain multisite)
if ( ! empty( $site_data->app_sub_type ) && 'subdom' === $site_data->app_sub_type && '*.' . $site_url === $alias ) {
continue;
}
// Replace *.domain with _wildcard.domain
if ( 0 === strpos( $alias, '*.' ) ) {
$domains[] = '_wildcard.' . substr( $alias, 2 );
} else {
$domains[] = $alias;
// For subdomain multisites, also add wildcard for non-wildcard alias domains
if ( ! empty( $site_data->app_sub_type ) && 'subdom' === $site_data->app_sub_type ) {
$domains[] = '_wildcard.' . $alias;
}
}
}
}

$auths = array_merge(
Auth::get_global_auths(),
Auth::where( 'site_url', $site_url )
);

foreach ( $auths as $key => $auth ) {
$flags = 'b';
// Remove duplicates (e.g., *.example.com alias + subdomain multisite both create _wildcard.example.com)
$domains = array_unique( $domains );
Comment on lines +307 to +345
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The domain collection logic in this method is duplicated in generate_site_whitelist (lines 412-445). Consider extracting this logic into a shared private method like collect_site_domains($site_url, $site_data) that returns the array of domains. This would reduce code duplication and make future maintenance easier.

Copilot uses AI. Check for mistakes.

// If no auths exist, remove all htpasswd files for this site and its domains
if ( empty( $auths ) ) {
foreach ( $domains as $domain ) {
$domain_auth_file = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/' . $domain;
$this->fs->remove( $domain_auth_file );
}
return;
}

// Generate htpasswd files for all collected domains
foreach ( $domains as $domain ) {
$domain_auth_file = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/' . $domain;
$this->fs->remove( $domain_auth_file );

if ( $key === 0 ) {
$flags = 'bc';
foreach ( $auths as $key => $auth ) {
$flags = 'b';

if ( $key === 0 ) {
$flags = 'bc';
}
EE::exec( sprintf( 'docker exec %s htpasswd -%s /etc/nginx/htpasswd/%s %s %s', EE_PROXY_TYPE, $flags, $domain, $auth->username, $auth->password ) );
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EE::exec builds a shell command string for docker exec htpasswd by directly interpolating untrusted username, password, and domain values without any escaping or argument separation. A user who can control --user/--pass (or stored auth entries) can include shell metacharacters in these fields so that, when this command is executed, arbitrary additional commands run on the host or in the proxy container. Replace this string-based command construction with a safe-by-default execution API that passes each argument separately or properly escapes every dynamic value before invoking the shell.

Suggested change
EE::exec( sprintf( 'docker exec %s htpasswd -%s /etc/nginx/htpasswd/%s %s %s', EE_PROXY_TYPE, $flags, $domain, $auth->username, $auth->password ) );
$container = escapeshellarg( EE_PROXY_TYPE );
$escaped_domain = escapeshellarg( $domain );
$escaped_username = escapeshellarg( $auth->username );
$escaped_password = escapeshellarg( $auth->password );
EE::exec(
sprintf(
'docker exec %s htpasswd -%s /etc/nginx/htpasswd/%s %s %s',
$container,
$flags,
$escaped_domain,
$escaped_username,
$escaped_password
)
);

Copilot uses AI. Check for mistakes.
}
EE::exec( sprintf( 'docker exec %s htpasswd -%s /etc/nginx/htpasswd/%s %s %s', EE_PROXY_TYPE, $flags, $site_url, $auth->username, $auth->password ) );
}
}

Expand All @@ -333,7 +388,10 @@ private function generate_global_whitelist() {
}

foreach ( $sites as $site ) {
$this->generate_site_whitelist( $site );
// Fetch site data to get app_sub_type and alias_domains
$site_info = \EE\Model\Site::where( 'site_url', $site );
$site_data = ! empty( $site_info ) ? $site_info[0] : null;
$this->generate_site_whitelist( $site, $site_data );
}

}
Expand All @@ -342,12 +400,49 @@ private function generate_global_whitelist() {
* Generates site whitelist files
*
* @param string $site_url
* @param object $site_data Optional site data object containing app_sub_type and alias_domains
*
* @throws Exception
*/
private function generate_site_whitelist( string $site_url ) {
$site_whitelist_file = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d/' . $site_url . '_acl';
$this->fs->remove( $site_whitelist_file );
private function generate_site_whitelist( string $site_url, $site_data = null ) {
// Always clean up wildcard file first (handles site type changes from subdom to regular)
$wildcard_file = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d/_wildcard.' . $site_url . '_acl';
$this->fs->remove( $wildcard_file );

// Collect all domains to generate whitelist files for
$domains = [ $site_url ];

// For subdomain multisites, add wildcard file
if ( $site_data && ! empty( $site_data->app_sub_type ) && 'subdom' === $site_data->app_sub_type ) {
$domains[] = '_wildcard.' . $site_url;
}

// Add alias domains (excluding main site_url)
if ( $site_data && ! empty( $site_data->alias_domains ) ) {
$alias_list = array_map( 'trim', explode( ',', $site_data->alias_domains ) );
foreach ( $alias_list as $alias ) {
if ( empty( $alias ) || $alias === $site_url ) {
continue;
}
// Skip *.site_url as it transforms to _wildcard.site_url (already added for subdomain multisite)
if ( ! empty( $site_data->app_sub_type ) && 'subdom' === $site_data->app_sub_type && '*.' . $site_url === $alias ) {
continue;
}
// Replace *.domain with _wildcard.domain
if ( 0 === strpos( $alias, '*.' ) ) {
$domains[] = '_wildcard.' . substr( $alias, 2 );
} else {
$domains[] = $alias;
// For subdomain multisites, also add wildcard for non-wildcard alias domains
if ( ! empty( $site_data->app_sub_type ) && 'subdom' === $site_data->app_sub_type ) {
$domains[] = '_wildcard.' . $alias;
}
}
}
}

// Remove duplicates
$domains = array_unique( $domains );

$whitelists = array_column(
'default' === $site_url ? Whitelist::get_global_ips() :
Expand All @@ -358,7 +453,21 @@ private function generate_site_whitelist( string $site_url ) {
'ip'
);

$this->put_ips_to_file( $site_whitelist_file, $whitelists );
// If no whitelists exist, remove all whitelist files for this site and its domains
if ( empty( $whitelists ) ) {
foreach ( $domains as $domain ) {
$domain_whitelist_file = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d/' . $domain . '_acl';
$this->fs->remove( $domain_whitelist_file );
}
return;
}

// Generate whitelist files for all collected domains
foreach ( $domains as $domain ) {
$domain_whitelist_file = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d/' . $domain . '_acl';
$this->fs->remove( $domain_whitelist_file );
$this->put_ips_to_file( $domain_whitelist_file, $whitelists );
}
}

/**
Expand Down Expand Up @@ -453,7 +562,7 @@ private function update_auth( array $assoc_args, string $site_url ) {
if ( 'default' === $site_url ) {
$this->generate_global_auth_files();
} else {
$this->generate_site_auth_files( $site_url );
$this->generate_site_auth_files( $site_url, $this->site_data );
}

EE::log( 'Reloading global reverse proxy.' );
Expand Down Expand Up @@ -499,7 +608,7 @@ private function update_whitelist( string $site_url, string $ips ) {
if ( 'default' === $site_url ) {
$this->generate_global_whitelist();
} else {
$this->generate_site_whitelist( $site_url );
$this->generate_site_whitelist( $site_url, $this->site_data );
}

reload_global_nginx_proxy();
Expand Down Expand Up @@ -594,7 +703,7 @@ public function delete( $args, $assoc_args ) {
if ( 'default' === $site_url ) {
$this->generate_global_auth_files();
} else {
$this->generate_site_auth_files( $site_url );
$this->generate_site_auth_files( $site_url, $this->site_data );
}

if ( $user ) {
Expand Down Expand Up @@ -646,7 +755,7 @@ public function delete( $args, $assoc_args ) {
if ( 'default' === $site_url ) {
$this->generate_global_whitelist();
} else {
$this->generate_site_whitelist( $site_url );
$this->generate_site_whitelist( $site_url, $this->site_data );
}

reload_global_nginx_proxy();
Expand Down