Skip to content

[3.0] Measures a display name against the column that has to hold it - #9619

Open
albertlast wants to merge 1 commit into
SimpleMachines:release-3.0from
albertlast:3.0/display-name-column-width
Open

[3.0] Measures a display name against the column that has to hold it#9619
albertlast wants to merge 1 commit into
SimpleMachines:release-3.0from
albertlast:3.0/display-name-column-width

Conversation

@albertlast

Copy link
Copy Markdown
Collaborator

Description

The third of the same mistake, found while working on #9616 and #9618: a length limit
counted in typed characters guarding a column that stores the entity encoded form of
them.

The display name limit is 60 characters. In the profile it is enforced by

if (Utils::entityStrlen($value) > 60) {
	return 'name_too_long';
}

Sources/Actions/Profile/Main.php runs Utils::htmlspecialcharsRecursive($_POST, ENT_QUOTES)
before validation, so $value is already encoded when it gets here, and entityStrlen()
decodes it again to count. That is the right number to show a member. It is not the number
members.real_name has to hold, which is the encoded one — six characters per double
quote, five per ampersand, four per angle bracket — in a varchar(255).

What happens

A display name of 43 double quotes is 43 characters to the validator and 258 in the
column. Run against the real validator and User::updateMemberData() on the Docker stack:

42 typed quotes: entityStrlen 42 (limit 60), encoded 252 chars (column 255), validator: true
43 typed quotes: entityStrlen 43 (limit 60), encoded 258 chars (column 255), validator: true
  Data too long for column 'real_name' at row 1
  File: /var/www/html/Sources/User.php  Line: 3420

The profile save dies on it. PostgreSQL gives value too long for type character varying(255) in the same place.

These characters really are allowed in a display name, which is the part that is easy to
get wrong: Security::validateUsername() rejects <>&"'=\, but that rule applies only to
member_name. The display name's validator forbids exactly one character, *, in
Security::isReservedName().

Registration has the same gap coming the other way. Register2 checks the raw value

&& Utils::entityStrlen($_POST['real_name']) < 60

and then encodes it thirty lines later when building extra_register_vars, so the value
that is measured and the value that is stored are again not the same string.

The fix

Check the encoded width alongside the typed length, in both places, so a name that cannot
be stored is refused by the form that offered it rather than by the database.

mb_strlen() rather than Utils::entityStrlen() is the point of the change: the encoded
length is what the column has to hold, and measuring it with entityStrlen() is the bug.

Why not widen the column instead

That is what #9616 does for the report comment, and it is the nicer answer where it is
available, because the limit the member is shown then means what it says. It is not
available here: real_name carries three indexes — idx_real_name,
idx_active_real_name, and idx_real_name_low on PostgreSQL — so it cannot become a
text column on MySQL without prefix lengths, and widening a members column that three
indexes and a great deal of code depend on is a much larger change than this bug warrants.

The cost of doing it this way is that a name of 43 double quotes is refused as "too long"
while the message says the limit is 60. That is a poor explanation, but it is a poor
explanation of a refusal rather than a fatal error, and it is only reachable by a name
made mostly of characters that need entities. I have deliberately left
$txt['profile_error_name_too_long'] alone rather than reword it and stale every
translation for this case; happy to change that if you would rather.

Verification

Against the patched validator, on a real database:

plain      typed  60, entityStrlen  60, encoded  60 -> true
42 quotes  typed  42, entityStrlen  42, encoded 252 -> true
43 quotes  typed  43, entityStrlen  43, encoded 258 -> 'name_too_long'
61 plain   typed  61, entityStrlen  61, encoded  61 -> 'name_too_long'

An ordinary 60 character name is still accepted, the widest name that actually fits is
still accepted, the one that used to fatal is now refused, and the original 60 character
limit still works.

composer lint and vendor/bin/phpunit (280 tests, 495 assertions) pass.

No unit test: the validator is a closure inside Profile::loadStandardFields(), which
needs a loaded member and therefore a database, and Register2 reads $_POST and queries
permissions. The numbers above came from running the real code against MySQL instead.

Issues References (Fixes|Related|Closes)

  1. Related to [3.0] Widens the report comment column to fit an entity encoded report #9616 and [3.0] Keeps a reported profile's name inside the column that stores it #9618 — same class of mistake, third column.

🤖 Generated with Claude Code

The display name limit is 60 characters, counted with entityStrlen() on
a value that has already been entity encoded, so it counts what was
typed. What real_name stores is the encoded form, where a double quote
is six characters and an ampersand five, and that column holds 255.

A name of 43 double quotes is therefore 43 characters to the validator,
comfortably inside the limit, and 258 in the column. Saving it dies with
"Data too long for column 'real_name'" out of User::updateMemberData().
Registration has the same gap from the other direction: it checks the
raw value against 60 and encodes it afterwards.

Checks the encoded width alongside the typed length in both places, so a
name that cannot be stored is refused by the form that offered it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant