diff --git a/classes/form/selectidp_buttons.php b/classes/form/selectidp_buttons.php index 54cf33108..4294dc358 100644 --- a/classes/form/selectidp_buttons.php +++ b/classes/form/selectidp_buttons.php @@ -82,7 +82,12 @@ public function definition() { * @return string */ private function get_idpbutton($idpentityid, $idpname, $logourl, $rememberedidp = false) { - $logo = !is_null($logourl) ? " " : ""; + $logo = ''; + $logourl = clean_param($logourl, PARAM_URL); + if (!is_null($logourl)) { + $logo = " "; + } + $idpname = clean_param($idpname, PARAM_TEXT); $extraclasses = $rememberedidp ? "rememberedidp" : ""; return << diff --git a/idp/sso.php b/idp/sso.php index fcd00ca9d..6f3c25fa1 100644 --- a/idp/sso.php +++ b/idp/sso.php @@ -53,9 +53,14 @@ // Get data from input request. $id = $xpath->evaluate('normalize-space(/*/@ID)'); -$destination = htmlspecialchars($xpath->evaluate('normalize-space(/*/@AssertionConsumerServiceURL)')); +$destinationraw = $xpath->evaluate('normalize-space(/*/@AssertionConsumerServiceURL)'); $sp = $xpath->evaluate('normalize-space(/*/*[local-name() = "Issuer"])'); +// Validate ID. +if (!auth_saml2_is_valid_saml_id($id)) { + throw new saml2_exception('invalid_id_error', get_string('invalid_id_error', 'auth_saml2', $id)); +} + // Confirm we know about this SP. $knownsps = []; foreach (explode(PHP_EOL, $saml2auth->config->moodleidpsplist) as $ksp) { @@ -70,6 +75,20 @@ throw new saml2_exception('unknown_sp_error', get_string('moodleidpsplist_error', 'auth_saml2', $sp)); } +// Validate ACS. +$destinationhost = parse_url($destinationraw, PHP_URL_HOST); +$sphost = parse_url($sp, PHP_URL_HOST); + +if (empty($destinationhost)) { + throw new saml2_exception('unknown_sp_error', get_string('moodleidpsplist_error', 'auth_saml2', $sp)); +} + +if ($saml2auth->config->acsmatchissuer && strcasecmp($destinationhost, $sphost) !== 0) { + throw new saml2_exception('acs_mismatch_error', get_string('acs_mismatch_error', 'auth_saml2')); +} + +$destination = htmlspecialchars($destinationraw); + // Get time in UTC. $datetime = new DateTime(); $datetime->setTimezone(new DatetimeZone('UTC')); diff --git a/lang/en/auth_saml2.php b/lang/en/auth_saml2.php index f0974abc5..bfabedb45 100644 --- a/lang/en/auth_saml2.php +++ b/lang/en/auth_saml2.php @@ -22,6 +22,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['acs_mismatch_error'] = 'ACS URL does not match issuer'; +$string['acsmatchissuer'] = 'Match ACS with issuer'; +$string['acsmatchissuer_help'] = 'If enabled, the Assertion Consumer Service (ACS) URL must match the issuer host. If you know that they do/should, enable this option to improve login security.'; $string['allowcreate'] = 'Allow create'; $string['allowcreate_help'] = 'Allow creation of IdP users on demand'; $string['alterlogout'] = 'Alternative Logout URL'; @@ -129,6 +132,7 @@ $string['idpname_help'] = 'eg myUNI - this is detected from the metadata and will show on the dual login page (if enabled)'; $string['idpnamedefault'] = 'Login via SAML2'; $string['idpnamedefault_varaible'] = 'Login via SAML2 ({$a})'; +$string['invalid_id_error'] = 'Invalid SAML ID'; $string['localityname'] = 'Locality'; $string['locked'] = 'Locked'; $string['logdir'] = 'Log Directory'; diff --git a/locallib.php b/locallib.php index 448c08cac..83dc73613 100644 --- a/locallib.php +++ b/locallib.php @@ -519,3 +519,14 @@ function auth_saml2_admin_nav($title, $url) { $PAGE->set_heading(get_string('pluginname', 'auth_saml2') . ': ' . $title); $PAGE->set_title(get_string('pluginname', 'auth_saml2') . ': ' . $title); } + +/** + * Validate a SAML protocol ID (xs:ID / NCName syntax) to prevent XML injection. + * + * @param string $id + * @return bool + */ +function auth_saml2_is_valid_saml_id(string $id): bool { + // NCName: starts with a letter or underscore, followed by letters, digits, '-', '_' or '.'. + return (bool) preg_match('/^[A-Za-z_][A-Za-z0-9_.-]*$/', $id) && strlen($id) <= 256; +} diff --git a/settings.php b/settings.php index 865ea42b5..7645bdb29 100644 --- a/settings.php +++ b/settings.php @@ -222,6 +222,14 @@ $acssetting->set_updatedcallback('auth_saml2_update_sp_metadata'); $settings->add($acssetting); + $settings->add(new admin_setting_configselect( + 'auth_saml2/acsmatchissuer', + get_string('acsmatchissuer', 'auth_saml2'), + get_string('acsmatchissuer_help', 'auth_saml2'), + 0, + $yesno, + )); + $settings->add(new admin_setting_configselect( 'auth_saml2/allowcreate', get_string('allowcreate', 'auth_saml2'), diff --git a/version.php b/version.php index 7d0ddb0ce..92e90bf3d 100644 --- a/version.php +++ b/version.php @@ -24,8 +24,8 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2026040202; // The current plugin version (Date: YYYYMMDDXX). -$plugin->release = 2026040202; // Match release exactly to version. +$plugin->version = 2026040203; // The current plugin version (Date: YYYYMMDDXX). +$plugin->release = 2026040203; // Match release exactly to version. $plugin->requires = 2025040400; // Requires Moodle 5.0 $plugin->component = 'auth_saml2'; // Full name of the plugin (used for diagnostics). $plugin->maturity = MATURITY_STABLE;