From c6e526279e0ff88e634ee65d2cadf2e1aae5348e Mon Sep 17 00:00:00 2001 From: ECYaz Date: Fri, 7 Aug 2026 11:57:01 -0400 Subject: [PATCH 1/2] Reject links entered as contribution names or permalinks A link pasted into the permalink field was rejected, but the error message suggested the slugified link back as a valid example, and pasting that suggestion in is accepted. A link entered as the contribution name with the permalink left blank produced the same garbage permalink silently. Both mistakes gave the contribution a permanent link shaped address only moderators can fix, which is how the ColoredLight permalink in #394 came to be. Reject values shaped like a link in the name and permalink fields with a clear message instead. Existing contributions with such a name or permalink still pass validation untouched, so they are not locked out of their manage page. --- controller/contribution/manage.php | 3 ++- includes/objects/contribution.php | 24 +++++++++++++++++++++++- language/en/contributions.php | 2 ++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/controller/contribution/manage.php b/controller/contribution/manage.php index 453c3f4d1..96df80834 100644 --- a/controller/contribution/manage.php +++ b/controller/contribution/manage.php @@ -216,7 +216,8 @@ public function manage($contrib_type, $contrib) $authors, $this->settings['custom'], $this->contrib->contrib_name_clean, - $this->settings['permalink'] + $this->settings['permalink'], + $old_settings['contrib_name'] )); // Did we succeed or have an error? diff --git a/includes/objects/contribution.php b/includes/objects/contribution.php index c4b74f3d0..5c1d97e23 100644 --- a/includes/objects/contribution.php +++ b/includes/objects/contribution.php @@ -1590,10 +1590,11 @@ public function change_permalink($new_permalink) * @param array $custom_fields Custom field values. * @param string $old_permalink Old permalink. Defaults to empty string. * @param string|null $new_permalink Submitted permalink. Defaults to the value on the entity. + * @param string $old_name Old contribution name. Defaults to empty string. * * @return array Returns array containing any errors found. */ - public function validate($contrib_categories, $authors, $custom_fields, $old_permalink = '', $new_permalink = null) + public function validate($contrib_categories, $authors, $custom_fields, $old_permalink = '', $new_permalink = null, $old_name = '') { phpbb::$user->add_lang('ucp'); @@ -1603,6 +1604,10 @@ public function validate($contrib_categories, $authors, $custom_fields, $old_per { $error[] = phpbb::$user->lang['EMPTY_CONTRIB_NAME']; } + else if ($this->contrib_name !== $old_name && $this->is_link($this->contrib_name)) + { + $error[] = phpbb::$user->lang['CONTRIB_NAME_IS_LINK']; + } $metadata = array( $this->contrib_name, @@ -1800,6 +1805,17 @@ protected function generate_permalink_slug($value) return preg_replace('/[^\p{L}\p{M}\p{N}_]+/u', '_', url::generate_slug($value)); } + /** + * Check whether a value is shaped like a link rather than a name. + * + * @param string $value + * @return bool + */ + protected function is_link($value) + { + return (bool) preg_match('#^(?:[a-z][a-z0-9+.\-]*://|www\.)#i', trim($value)); + } + /* * Validate a contrib permalink * @@ -1816,6 +1832,12 @@ public function validate_permalink($permalink, $old_permalink) return false; } + // A pasted link must not be echoed back as a valid permalink example. + if ($this->is_link($permalink)) + { + return phpbb::$user->lang['CONTRIB_PERMALINK_IS_LINK']; + } + $generated_permalink = $this->generate_permalink_slug($permalink); if ($generated_permalink !== $permalink) { diff --git a/language/en/contributions.php b/language/en/contributions.php index 3de7f78fc..5a6504821 100644 --- a/language/en/contributions.php +++ b/language/en/contributions.php @@ -79,6 +79,7 @@ 'CONTRIB_LOCAL_NAME' => 'Local name', 'CONTRIB_LOCAL_NAME_EXPLAIN' => 'The localized name of the language, e.g. Français.', 'CONTRIB_NAME' => 'Contribution Name', + 'CONTRIB_NAME_IS_LINK' => 'The contribution name cannot be a link. Please enter the name of your contribution.', 'CONTRIB_NEW' => 'New', 'CONTRIB_NONACTIVE_AUTHORS' => 'Non-Active Co-Authors (Past Contributors)', 'CONTRIB_NONACTIVE_AUTHORS_EXPLAIN' => 'Non-Active Co-Authors can not manage anything for the contribution and are only listed as previous authors.', @@ -87,6 +88,7 @@ 'CONTRIB_PERMALINK' => 'Contribution Permalink', 'CONTRIB_PERMALINK_EXISTS' => 'That contribution permalink is already in use.', 'CONTRIB_PERMALINK_EXPLAIN' => 'Cleaned version of the contribution name, used to build the url for the contribution.
Leave blank to have one automatically created based on the contribution name.', + 'CONTRIB_PERMALINK_IS_LINK' => 'The permalink cannot be a link. It is the short address of the contribution page, built from the contribution name.', 'CONTRIB_RELEASE_DATE' => 'Release date', 'CONTRIB_STATUS' => 'Contribution status', 'CONTRIB_STATUS_EXPLAIN' => 'Change the contribution status', From 8bf264b492ea4bdaa5525e96566e94585e01bbdf Mon Sep 17 00:00:00 2001 From: ECYaz Date: Mon, 10 Aug 2026 13:34:36 -0400 Subject: [PATCH 2/2] Validate new permalinks against a strict allowlist Replace the link shape detection on the permalink field with a strict character allowlist: a new permalink may only contain letters, combining marks, numbers and underscores, the exact set the permalink generator itself produces. A pasted link now fails on its punctuation before any slugified version of it can be echoed back as a valid example. The canonical form check behind it still lowercases and cleans input that passes the allowlist, where echoing the corrected permalink back is safe and helpful. The contribution name field keeps the link shape check, since a name legitimately contains spaces and punctuation and cannot be allowlisted the same way. --- includes/objects/contribution.php | 8 +++++--- language/en/contributions.php | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/includes/objects/contribution.php b/includes/objects/contribution.php index 5c1d97e23..a69fd12dc 100644 --- a/includes/objects/contribution.php +++ b/includes/objects/contribution.php @@ -1832,10 +1832,12 @@ public function validate_permalink($permalink, $old_permalink) return false; } - // A pasted link must not be echoed back as a valid permalink example. - if ($this->is_link($permalink)) + // Only allow the characters the permalink generator itself produces. + // A pasted link fails this outright, so no slugified link is ever + // echoed back as a valid permalink example. + if (!preg_match('/^[\p{L}\p{M}\p{N}_]*$/u', $permalink)) { - return phpbb::$user->lang['CONTRIB_PERMALINK_IS_LINK']; + return phpbb::$user->lang['CONTRIB_PERMALINK_INVALID_CHARACTERS']; } $generated_permalink = $this->generate_permalink_slug($permalink); diff --git a/language/en/contributions.php b/language/en/contributions.php index 5a6504821..9139a2763 100644 --- a/language/en/contributions.php +++ b/language/en/contributions.php @@ -88,7 +88,7 @@ 'CONTRIB_PERMALINK' => 'Contribution Permalink', 'CONTRIB_PERMALINK_EXISTS' => 'That contribution permalink is already in use.', 'CONTRIB_PERMALINK_EXPLAIN' => 'Cleaned version of the contribution name, used to build the url for the contribution.
Leave blank to have one automatically created based on the contribution name.', - 'CONTRIB_PERMALINK_IS_LINK' => 'The permalink cannot be a link. It is the short address of the contribution page, built from the contribution name.', + 'CONTRIB_PERMALINK_INVALID_CHARACTERS' => 'The permalink may only contain letters, numbers and underscores. Leave the field blank to have a valid one created from the contribution name.', 'CONTRIB_RELEASE_DATE' => 'Release date', 'CONTRIB_STATUS' => 'Contribution status', 'CONTRIB_STATUS_EXPLAIN' => 'Change the contribution status',