Commit ef3fe076848 for woocommerce

commit ef3fe076848e3e54be2388a03d2d22db347b1d2c
Author: Bruna <bruna.filippozzi@automattic.com>
Date:   Mon Jul 6 17:01:50 2026 +0200

    Fix attribute slug length validation for multibyte characters (#64159)

    * Fix attribute slug length validation for multibyte characters

    strlen() counts bytes, not characters. Cyrillic characters are 2 bytes
    in UTF-8, so a 21-character slug incorrectly returns 42 and gets rejected.
    Use mb_strlen() which counts characters, matching how MySQL varchar(32)
    measures length. Fixed in both wc-attribute-functions.php and the REST
    API v1 controller.

    Fixes woocommerce/woocommerce#36892

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Update wc-attribute-functions.php

    * Update wc-attribute-functions.php

    * Switch slug-length check to byte-based pa_+slug ≤ 32

    Address review feedback from @zhongruige. The previous mb_strlen-based
    check accepted multibyte slugs that WordPress would later reject:
    register_taxonomy() (wp-includes/taxonomy.php) gates on strlen of the
    full taxonomy name, with a 32-byte limit. WooCommerce prefixes the slug
    with "pa_", so the byte budget for the slug itself is 29.

    A 20-character Cyrillic slug ("размерпродуктаодежды") is 40 bytes,
    which mb_strlen reports as 20 — under our previous 28-char limit, so
    WooCommerce would accept it, write the term to the database (varchar(32)
    counts characters in utf8mb4), then fail when register_taxonomy() runs
    later, leaving the attribute in a broken state.

    The new check matches WordPress exactly:

      if ( strlen( 'pa_' . $slug ) > 32 ) { ... }

    This also drops the dependency on mb_internal_encoding (which
    class-wc-geo-ip.php mutates to ISO-8859-1), so the check is
    deterministic regardless of global state.

    Tests cover the new boundaries: 29-byte ASCII, 14-char Cyrillic,
    9-char Chinese (all accepted) and 30-byte ASCII, 15-char Cyrillic,
    10-char Chinese (all rejected). The PHPStan baseline entry for the
    prior strlen call is removed because the check now operates on a
    guaranteed-string concatenation.

    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

    * Refine slug UI copy and simplify validation error messaging

    Address the remaining review items from @zhongruige:

    - Update the admin attribute slug input maxlength from 28 to 29 (the
      byte budget after the "pa_" prefix) and rewrite the help text. The
      previous "must be no more than N characters" copy was a small lie:
      HTML maxlength counts UTF-16 code units, so a 29-character Cyrillic
      slug (58 bytes) would still pass the input check before failing on
      the server. The new copy explains that multibyte characters count as
      multiple bytes, leaving the server-side check as the source of truth.

    - Extract $prefixed_slug_byte_length so strlen( 'pa_' . $slug ) runs
      once per validation call in both wc-attribute-functions.php and the
      V1 REST controller, instead of being computed in the condition and
      again in the error payload.

    - Simplify the user-facing error to "Slug %s is too long. Please use
      a shorter slug." Byte counts and the "pa_" prefix detail are no
      longer surfaced in translated copy; they move into the WP_Error
      data array as slug_byte_length / slug_byte_length_limit for API
      consumers that want them.

    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

    * Report user slug bytes (not prefixed taxonomy bytes) in WP_Error data

    The previous WP_Error data array was misleading: slug_byte_length
    held strlen( 'pa_' . $slug ) and slug_byte_length_limit was 32, so
    an API consumer sending a 30-byte slug would see slug_byte_length: 33,
    slug_byte_length_limit: 32 and have to figure out why their input
    appeared 3 bytes longer than they sent.

    Subtract the 3-byte 'pa_' prefix from both values so the data array
    describes the slug the consumer actually submitted: a 30-byte slug
    now reports slug_byte_length: 30 against slug_byte_length_limit: 29.
    The condition still operates on the prefixed taxonomy name, matching
    the register_taxonomy() check in wp-includes/taxonomy.php.

    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

    * Polish slug validation: drop magic numbers and broaden description copy

    Two small follow-ups on top of the previous commits:

    - Replace the bare `- 3` and `29` in the WP_Error data array with
      `- strlen( 'pa_' )` and `32 - strlen( 'pa_' )`. The arithmetic is
      identical (PHP folds strlen on string literals), but the relationship
      to the WordPress 32-byte taxonomy name limit and the `pa_` prefix is
      now self-documenting at the call site, not implicit.

    - Rewrite the admin slug description from "multibyte characters (such
      as Cyrillic or Chinese) count as multiple bytes" to "non-ASCII
      characters use multiple bytes". The old copy singled out two scripts
      out of many that are multibyte (Arabic, Hebrew, Thai, Japanese,
      Korean, Devanagari, etc., and accented Latin like é). "Non-ASCII" is
      precise: in UTF-8, the ASCII range is exactly the 1-byte range and
      everything else is 2-4 bytes.

    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

    * Make slug description concrete: name the byte limit and per-character cost

    Tightens the admin slug input description from "Keep it short:
    non-ASCII characters use multiple bytes" to "May be up to 29 bytes;
    non-ASCII characters each count as 2-4 bytes." The new copy gives
    users a concrete number to reason about and explains the per-character
    cost, so a multibyte user can estimate roughly how many characters
    will fit without trial-and-error.

    This is still a copy-only improvement; the underlying UX gap (HTML
    maxlength cannot enforce a byte limit) requires a JS input handler
    that counts TextEncoder bytes as the user types, which is being
    tracked separately.

    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

    * Add live byte counter to attribute slug input

    The HTML maxlength attribute counts characters, not bytes, so a user
    typing 15 Cyrillic characters (15 chars, 30 bytes) silently passes the
    browser's guard and is rejected only by the server-side 29-byte check
    on submit. This adds an inline JS handler that uses TextEncoder to
    count UTF-8 bytes as the user types, appending a "<n> / 29 bytes"
    indicator below the existing description that turns amber within 3
    bytes of the limit and red when over. The handler runs on both the
    add and edit screens via a shared private static helper.

    Addresses Greg's review feedback on PR #64159.

    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

    * fix: guard inline slug counter against JSON encoding failure

    The admin attribute slug byte counter echoes wp_json_encode()'s result
    directly into an inline <script>. wp_json_encode() returns false on
    encoding failure (e.g. invalid UTF-8 in a translated string), which PHP
    renders as an empty string, producing `var template = ;` — a syntax
    error that silently disables the counter.

    Guard the false return and skip the counter rather than emit broken
    JavaScript, and pass JSON_HEX_TAG | JSON_HEX_AMP so angle brackets and
    ampersands in the translated template are hex-encoded, matching the
    escaping used elsewhere in the codebase for JSON embedded in <script>.

    Refs WOOPLUG-1090

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    * refactor: consolidate attribute slug byte-length limit into a shared helper

    The 32-byte taxonomy limit and 'pa_' prefix were hardcoded in three
    places — wc_create_attribute(), the REST v1 validate_attribute_slug(), and
    the admin slug byte counter — each computing strlen( 'pa_' . $slug ) > 32
    independently. The WP_Error data also derived the slug length and limit via
    strlen( 'pa_' ) arithmetic that obscured the plain values (strlen( $slug )
    and 29).

    Add wc_get_attribute_slug_max_byte_length() as the single source of truth
    and use it everywhere, validating strlen( $slug ) directly against it and
    reporting the slug's own byte length in the error data. Cast the sanitized
    slug to string so the simplified strlen() stays free of the PHPStan
    null-argument warning the prefixed form previously masked. Document the JS
    amber-warning threshold (within one multibyte character of the limit).

    Refs WOOPLUG-1090

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    * test: clarify multibyte slug boundary coverage

    The new Cyrillic and Chinese boundary cases use 28/30 and 27/30-byte slugs
    because a pure multibyte slug can't measure exactly 29 bytes — Cyrillic is
    2 bytes/char and these CJK characters are 3. Without that context the cases
    read as if they exercise the boundary directly.

    Document that the exact 29-byte limit is covered by the ASCII case in the
    modern suite, and that the multibyte cases cover the closest reachable
    values just under and just over the limit.

    Refs WOOPLUG-1090

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    * chore: drop PHPStan baseline entries resolved by the slug string cast

    Casting the sanitized slug to string in wc_create_attribute() (so the
    simplified strlen() stays clean) also made $slug a plain string everywhere
    downstream, resolving four previously-baselined "string|null" errors in
    wc-attribute-functions.php: the $old_slug phpDoc type and the string|null
    arguments into wc_attribute_taxonomy_name(), wc_check_if_attribute_name_is_reserved(),
    and sanitize_title().

    PHPStan's CI gate fails when the baseline carries entries that no longer
    match, and the project policy is that the baseline only shrinks. Remove the
    four stale entries so it stays in sync. Found only by the full CI run —
    per-file local analysis disables unmatched-baseline reporting.

    Refs WOOPLUG-1090

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---------

    Co-authored-by: Bruna <bruberries@MacBook-Pro-9.local>
    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
    Co-authored-by: Greg <71906536+zhongruige@users.noreply.github.com>
    Co-authored-by: Bruna Filippozzi <bruna.filippozzi@a8c.com>
    Co-authored-by: Vlad Olaru <vlad.olaru@automattic.com>
    Co-authored-by: Vlad Olaru <vlad@thinkwritecode.com>

diff --git a/plugins/woocommerce/changelog/fix-attribute-slug-length-multibyte b/plugins/woocommerce/changelog/fix-attribute-slug-length-multibyte
new file mode 100644
index 00000000000..39a17355549
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-attribute-slug-length-multibyte
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Reject attribute slugs whose taxonomy name (with the "pa_" prefix) exceeds WordPress's 32-byte limit, instead of incorrectly counting characters. This lets users create attributes with multibyte slugs (Cyrillic, Chinese, etc.) that fit within the limit, and shows a live byte count next to the slug input so users can see how close they are to the limit before submitting.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php b/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php
index 9157f2d5ca6..7b4971f18a4 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-attributes.php
@@ -159,6 +159,70 @@ class WC_Admin_Attributes {
 		return wc_delete_attribute( $attribute_id );
 	}

+	/**
+	 * Output an inline script that shows a live UTF-8 byte count next to the
+	 * slug input and visually warns when the value approaches or exceeds the
+	 * server-side byte limit.
+	 *
+	 * The HTML `maxlength` attribute counts characters, not bytes, so a
+	 * multibyte slug (e.g. Cyrillic, Chinese) can pass the browser's guard
+	 * and still be rejected by `register_taxonomy()`'s 32-byte name limit.
+	 * This counter closes that feedback gap before submission.
+	 */
+	private static function slug_byte_counter_script(): void {
+		$max_bytes = wc_get_attribute_slug_max_byte_length();
+		/* translators: 1: current byte count, 2: maximum allowed bytes. */
+		$template = wp_json_encode( __( '%1$d / %2$d bytes', 'woocommerce' ), JSON_HEX_TAG | JSON_HEX_AMP );
+		if ( false === $template ) {
+			// Encoding the counter template failed (e.g. invalid UTF-8 in the translated
+			// string); skip the counter rather than emit broken inline JavaScript.
+			return;
+		}
+		?>
+		<script type="text/javascript">
+			( function() {
+				var input = document.getElementById( 'attribute_name' );
+				if ( ! input || ! ( 'TextEncoder' in window ) ) {
+					return;
+				}
+				var wrapper = input.closest( 'td, .form-field' );
+				var description = wrapper ? wrapper.querySelector( 'p.description' ) : null;
+				if ( ! description ) {
+					return;
+				}
+				var maxBytes = <?php echo (int) $max_bytes; ?>;
+				var template = <?php echo $template; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode with JSON_HEX_TAG produces JS-safe output. ?>;
+				var encoder = new TextEncoder();
+				var counter = document.createElement( 'span' );
+				counter.style.display = 'block';
+				counter.style.marginTop = '4px';
+				description.appendChild( counter );
+				function update() {
+					var bytes = encoder.encode( input.value ).length;
+					if ( 0 === bytes ) {
+						counter.textContent = '';
+						counter.style.color = '';
+						return;
+					}
+					counter.textContent = template
+						.replace( '%1$d', bytes )
+						.replace( '%2$d', maxBytes );
+					if ( bytes > maxBytes ) {
+						counter.style.color = '#d63638';
+					} else if ( bytes >= maxBytes - 3 ) {
+						// Within one multibyte character (up to 3 bytes) of the limit.
+						counter.style.color = '#996800';
+					} else {
+						counter.style.color = '';
+					}
+				}
+				input.addEventListener( 'input', update );
+				update();
+			} )();
+		</script>
+		<?php
+	}
+
 	/**
 	 * Edit Attribute admin panel.
 	 *
@@ -215,8 +279,8 @@ class WC_Admin_Attributes {
 									<label for="attribute_name"><?php esc_html_e( 'Slug', 'woocommerce' ); ?></label>
 								</th>
 								<td>
-									<input name="attribute_name" id="attribute_name" type="text" value="<?php echo esc_attr( $att_name ); ?>" maxlength="28" />
-									<p class="description"><?php esc_html_e( 'Unique slug/reference for the attribute; must be no more than 28 characters.', 'woocommerce' ); ?></p>
+									<input name="attribute_name" id="attribute_name" type="text" value="<?php echo esc_attr( $att_name ); ?>" maxlength="29" />
+									<p class="description"><?php esc_html_e( 'Unique slug/reference for the attribute. May be up to 29 bytes; non-ASCII characters each count as 2–4 bytes.', 'woocommerce' ); ?></p>
 								</td>
 							</tr>
 							<tr class="form-field form-required">
@@ -283,6 +347,7 @@ class WC_Admin_Attributes {
 					<p class="submit"><button type="submit" name="save_attribute" id="submit" class="button-primary" value="<?php esc_attr_e( 'Update', 'woocommerce' ); ?>"><?php esc_html_e( 'Update', 'woocommerce' ); ?></button></p>
 					<?php wp_nonce_field( 'woocommerce-save-attribute_' . $edit ); ?>
 				</form>
+				<?php self::slug_byte_counter_script(); ?>
 				<?php
 			}//end if
 			?>
@@ -433,8 +498,8 @@ class WC_Admin_Attributes {

 								<div class="form-field">
 									<label for="attribute_name"><?php esc_html_e( 'Slug', 'woocommerce' ); ?></label>
-									<input name="attribute_name" id="attribute_name" type="text" value="" maxlength="28" />
-									<p class="description"><?php esc_html_e( 'Unique slug/reference for the attribute; must be no more than 28 characters.', 'woocommerce' ); ?></p>
+									<input name="attribute_name" id="attribute_name" type="text" value="" maxlength="29" />
+									<p class="description"><?php esc_html_e( 'Unique slug/reference for the attribute. May be up to 29 bytes; non-ASCII characters each count as 2–4 bytes.', 'woocommerce' ); ?></p>
 								</div>

 								<div class="form-field">
@@ -507,6 +572,7 @@ class WC_Admin_Attributes {

 			/* ]]> */
 			</script>
+			<?php self::slug_byte_counter_script(); ?>
 		</div>
 		<?php
 	}
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php
index 8f4804dd857..f12e3d6103e 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php
@@ -604,9 +604,21 @@ class WC_REST_Product_Attributes_V1_Controller extends WC_REST_Controller {
 	 * @return bool|WP_Error
 	 */
 	protected function validate_attribute_slug( $slug, $new_data = true ) {
-		if ( strlen( $slug ) > 28 ) {
-			/* translators: %s: slug being validated */
-			return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
+		// Validate slug length against the byte budget left by WordPress's 32-byte taxonomy
+		// name limit once the 'pa_' prefix is accounted for (see wc_get_attribute_slug_max_byte_length()).
+		$slug_byte_length = strlen( $slug );
+		$max_byte_length  = wc_get_attribute_slug_max_byte_length();
+		if ( $slug_byte_length > $max_byte_length ) {
+			return new WP_Error(
+				'woocommerce_rest_invalid_product_attribute_slug_too_long',
+				/* translators: %s: slug being validated */
+				sprintf( __( 'Slug "%s" is too long. Please use a shorter slug.', 'woocommerce' ), $slug ),
+				array(
+					'status'                 => 400,
+					'slug_byte_length'       => $slug_byte_length,
+					'slug_byte_length_limit' => $max_byte_length,
+				)
+			);
 		} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
 			/* translators: %s: slug being validated */
 			return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
diff --git a/plugins/woocommerce/includes/wc-attribute-functions.php b/plugins/woocommerce/includes/wc-attribute-functions.php
index d76cd077dda..cc60db61341 100644
--- a/plugins/woocommerce/includes/wc-attribute-functions.php
+++ b/plugins/woocommerce/includes/wc-attribute-functions.php
@@ -144,6 +144,22 @@ function wc_attribute_taxonomy_name( $attribute_name ) {
 	return $attribute_name ? 'pa_' . wc_sanitize_taxonomy_name( $attribute_name ) : '';
 }

+/**
+ * Get the maximum byte length allowed for a product attribute slug.
+ *
+ * WordPress's register_taxonomy() rejects taxonomy names longer than 32 bytes
+ * (see wp-includes/taxonomy.php), and WooCommerce prefixes attribute slugs with
+ * 'pa_'. That leaves the slug itself a budget of 32 bytes minus the prefix length.
+ * The limit is measured in bytes, not characters: multibyte characters (Cyrillic,
+ * Chinese, etc.) consume 2-4 bytes each.
+ *
+ * @since 11.0.0
+ * @return int Maximum attribute slug length, in bytes.
+ */
+function wc_get_attribute_slug_max_byte_length() {
+	return 32 - strlen( 'pa_' );
+}
+
 /**
  * Get the attribute name used when storing values in post meta.
  *
@@ -508,13 +524,24 @@ function wc_create_attribute( $args ) {
 	if ( empty( $args['slug'] ) ) {
 		$slug = wc_sanitize_taxonomy_name( $args['name'] );
 	} else {
-		$slug = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( $args['slug'] ) );
+		$slug = (string) preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( $args['slug'] ) );
 	}

-	// Validate slug.
-	if ( strlen( $slug ) > 28 ) {
-		/* translators: %s: attribute slug */
-		return new WP_Error( 'invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
+	// Validate slug length against the byte budget left by WordPress's 32-byte taxonomy
+	// name limit once the 'pa_' prefix is accounted for (see wc_get_attribute_slug_max_byte_length()).
+	$slug_byte_length = strlen( $slug );
+	$max_byte_length  = wc_get_attribute_slug_max_byte_length();
+	if ( $slug_byte_length > $max_byte_length ) {
+		return new WP_Error(
+			'invalid_product_attribute_slug_too_long',
+			/* translators: %s: attribute slug */
+			sprintf( __( 'Slug "%s" is too long. Please use a shorter slug.', 'woocommerce' ), $slug ),
+			array(
+				'status'                 => 400,
+				'slug_byte_length'       => $slug_byte_length,
+				'slug_byte_length_limit' => $max_byte_length,
+			)
+		);
 	} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
 		/* translators: %s: attribute slug */
 		return new WP_Error( 'invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 09ad470e704..808f2739efb 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -33039,12 +33039,6 @@ parameters:
 			count: 2
 			path: includes/wc-account-functions.php

-		-
-			message: '#^@param string \$old_slug does not accept actual type of parameter\: string\|null\.$#'
-			identifier: parameter.phpDocType
-			count: 1
-			path: includes/wc-attribute-functions.php
-
 		-
 			message: '#^Argument of an invalid type array\<int, WP_Term\>\|WP_Error supplied for foreach, only iterables are supported\.$#'
 			identifier: foreach.nonIterable
@@ -33099,30 +33093,6 @@ parameters:
 			count: 1
 			path: includes/wc-attribute-functions.php

-		-
-			message: '#^Parameter \#1 \$attribute_name of function wc_attribute_taxonomy_name expects string, string\|null given\.$#'
-			identifier: argument.type
-			count: 3
-			path: includes/wc-attribute-functions.php
-
-		-
-			message: '#^Parameter \#1 \$attribute_name of function wc_check_if_attribute_name_is_reserved expects string, string\|null given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/wc-attribute-functions.php
-
-		-
-			message: '#^Parameter \#1 \$string of function strlen expects string, string\|null given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/wc-attribute-functions.php
-
-		-
-			message: '#^Parameter \#1 \$title of function sanitize_title expects string, string\|null given\.$#'
-			identifier: argument.type
-			count: 3
-			path: includes/wc-attribute-functions.php
-
 		-
 			message: '#^Access to an undefined property object\:\:\$name\.$#'
 			identifier: property.notFound
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/attributes/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/attributes/functions.php
index f25eea532b6..b382889b191 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/attributes/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/attributes/functions.php
@@ -58,6 +58,49 @@ class WC_Tests_Attributes_Functions extends WC_Unit_Test_Case {
 		$err = wc_create_attribute( array( 'name' => 'This is a big name for a product attribute!' ) );
 		$this->assertEquals( 'invalid_product_attribute_slug_too_long', $err->get_error_code() );

+		// Multibyte boundary cases. The slug byte budget is 29 (pa_ + 29 = 32). A pure
+		// Cyrillic/CJK slug can't measure exactly 29 bytes — Cyrillic is 2 bytes/char and
+		// these CJK characters are 3 bytes/char — so these cover the closest reachable values
+		// on either side (the exact 29-byte boundary is covered by the ASCII case in the
+		// modern tests/php/includes/wc-attribute-functions-test.php suite).
+		// 14-char Cyrillic slug = 28 bytes; with 'pa_' prefix = 31 bytes (under WP's 32-byte taxonomy limit).
+		$cyrillic_ok = wc_create_attribute(
+			array(
+				'slug' => 'абвгдежзиклмно',
+				'name' => 'OK Cyrillic',
+			)
+		);
+		$this->assertIsInt( $cyrillic_ok );
+		wc_delete_attribute( $cyrillic_ok );
+
+		// 15-char Cyrillic slug = 30 bytes; with 'pa_' prefix = 33 bytes — must be rejected.
+		$err = wc_create_attribute(
+			array(
+				'slug' => 'абвгдежзиклмноп',
+				'name' => 'Too long Cyrillic',
+			)
+		);
+		$this->assertEquals( 'invalid_product_attribute_slug_too_long', $err->get_error_code() );
+
+		// 9-char Chinese slug = 27 bytes; with 'pa_' prefix = 30 bytes — under the limit.
+		$chinese_ok = wc_create_attribute(
+			array(
+				'slug' => '尺寸大小颜色品牌型',
+				'name' => 'OK Chinese',
+			)
+		);
+		$this->assertIsInt( $chinese_ok );
+		wc_delete_attribute( $chinese_ok );
+
+		// 10-char Chinese slug = 30 bytes; with 'pa_' prefix = 33 bytes — must be rejected.
+		$err = wc_create_attribute(
+			array(
+				'slug' => '尺寸大小颜色品牌型号',
+				'name' => 'Too long Chinese',
+			)
+		);
+		$this->assertEquals( 'invalid_product_attribute_slug_too_long', $err->get_error_code() );
+
 		$err = wc_create_attribute( array( 'name' => 'Cat' ) );
 		$this->assertEquals( 'invalid_product_attribute_slug_reserved_name', $err->get_error_code() );

diff --git a/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
index d1150ed54cc..50944eaeeb8 100644
--- a/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
@@ -129,10 +129,38 @@ class WC_Attribute_Functions_Test extends \WC_Unit_Test_Case {
 			'wc_create_attribute should return a numeric id on success.'
 		);

-		$ids[] = wc_create_attribute( array( 'name' => str_repeat( 'n', 28 ) ) );
+		// This 29-byte ASCII slug exercises the exact upper boundary (pa_ + 29 = 32 bytes).
+		// The multibyte cases below can't land on 29 bytes exactly — Cyrillic is 2 bytes/char
+		// (so 28 or 30) and these CJK characters are 3 bytes/char (27 or 30) — so they cover
+		// the closest reachable values just under and just over the limit.
+		$ids[] = wc_create_attribute( array( 'name' => str_repeat( 'n', 29 ) ) );
 		$this->assertIsInt(
 			end( $ids ),
-			'Attribute creation should succeed when its slug is 28 characters long.'
+			'Attribute creation should succeed when its 29-byte slug fits in the 32-byte taxonomy limit (with the "pa_" prefix).'
+		);
+
+		// 14-char Cyrillic slug = 28 bytes; with 'pa_' prefix = 31 bytes (within the 32-byte WP taxonomy limit).
+		$ids[] = wc_create_attribute(
+			array(
+				'slug' => 'абвгдежзиклмно',
+				'name' => 'OK Cyrillic',
+			)
+		);
+		$this->assertIsInt(
+			end( $ids ),
+			'Attribute creation should succeed for a 14-character Cyrillic slug (28 bytes).'
+		);
+
+		// 9-char Chinese slug = 27 bytes; with 'pa_' prefix = 30 bytes (within the limit).
+		$ids[] = wc_create_attribute(
+			array(
+				'slug' => '尺寸大小颜色品牌型',
+				'name' => 'OK Chinese',
+			)
+		);
+		$this->assertIsInt(
+			end( $ids ),
+			'Attribute creation should succeed for a 9-character Chinese slug (27 bytes).'
 		);

 		$err = wc_create_attribute( array() );
@@ -142,11 +170,37 @@ class WC_Attribute_Functions_Test extends \WC_Unit_Test_Case {
 			'Attributes should not be allowed to be created without specifying a name.'
 		);

-		$err = wc_create_attribute( array( 'name' => str_repeat( 'n', 29 ) ) );
+		$err = wc_create_attribute( array( 'name' => str_repeat( 'n', 30 ) ) );
+		$this->assertEquals(
+			'invalid_product_attribute_slug_too_long',
+			$err->get_error_code(),
+			'Attribute slugs whose prefixed taxonomy name (pa_<slug>) exceeds 32 bytes should be rejected.'
+		);
+
+		// 15-char Cyrillic slug = 30 bytes; with 'pa_' prefix = 33 bytes — must be rejected.
+		$err = wc_create_attribute(
+			array(
+				'slug' => 'абвгдежзиклмноп',
+				'name' => 'Too long Cyrillic',
+			)
+		);
+		$this->assertEquals(
+			'invalid_product_attribute_slug_too_long',
+			$err->get_error_code(),
+			'A 15-character Cyrillic slug (30 bytes) should be rejected because pa_<slug> exceeds 32 bytes.'
+		);
+
+		// 10-char Chinese slug = 30 bytes; with 'pa_' prefix = 33 bytes — must be rejected.
+		$err = wc_create_attribute(
+			array(
+				'slug' => '尺寸大小颜色品牌型号',
+				'name' => 'Too long Chinese',
+			)
+		);
 		$this->assertEquals(
 			'invalid_product_attribute_slug_too_long',
 			$err->get_error_code(),
-			'Attribute slugs should not be allowed to be over 28 characters long.'
+			'A 10-character Chinese slug (30 bytes) should be rejected because pa_<slug> exceeds 32 bytes.'
 		);

 		$err = wc_create_attribute( array( 'name' => 'Cat' ) );