Skip to content
Merged
Show file tree
Hide file tree
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
17 changes: 17 additions & 0 deletions src/Intl/Idn/Idn.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ final class Idn
public const DELIMITER = '-';
public const MAX_INT = 2147483647;

/**
* Punycode decoding does work quadratic in the payload length. Valid ACE
* labels are limited to 63 bytes, so payloads beyond this (generous) bound
* are rejected without being decoded to keep the work bounded.
*/
private const MAX_DECODE_PAYLOAD_SIZE = 1024;

/**
* Contains the numeric value of a basic code point (for use in representing integers) in the
* range 0 to BASE-1, or -1 if b is does not represent a value.
Expand Down Expand Up @@ -358,6 +365,16 @@ private static function process($domain, array $options, Info $info)
continue;
}

// The decoder does work quadratic in the payload length. Valid
// labels are at most 63 bytes long, so a payload beyond this
// bound is always invalid input: reject it without decoding,
// like ext-intl does, to avoid spending unbounded time on it.
if (\strlen($label) - 4 > self::MAX_DECODE_PAYLOAD_SIZE) {
$info->errors |= self::ERROR_PUNYCODE;

continue;
}

// Step 4.2. Attempt to convert the rest of the label to Unicode according to Punycode [RFC3492]. If
// that conversion fails, record that there was an error, and continue
// with the next label. Otherwise replace the original label in the string by the results of the
Expand Down
39 changes: 39 additions & 0 deletions tests/Intl/Idn/IdnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,45 @@ public function testLabelStartingWithPunycodePrefixWithNonAsciiCharacterRecordsE
$this->assertSame('xn--🌈.faß.de', $info['result']);
}

/**
* Punycode decoding does work quadratic in the payload length. Valid ACE
* labels are limited to 63 bytes, so a multi-kilobyte payload is always
* invalid input and must be rejected without being decoded (like ext-intl,
* which never decodes absurdly long labels).
*/
public function testHugeAcePayloadIsRejectedWithoutDecoding()
{
$input = 'example.xn--'.str_repeat('w', 5000).'.com';

$info = [];
$r = idn_to_utf8($input, \IDNA_DEFAULT, \INTL_IDNA_VARIANT_UTS46, $info);
$this->assertFalse($r);
// ext-intl does not populate $info when it bails out early.
if ([] !== $info) {
$this->assertSame(\IDNA_ERROR_PUNYCODE, \IDNA_ERROR_PUNYCODE & $info['errors']);
}

$info = [];
$r = idn_to_ascii($input, \IDNA_DEFAULT, \INTL_IDNA_VARIANT_UTS46, $info);
$this->assertFalse($r);
if ([] !== $info) {
$this->assertSame(\IDNA_ERROR_PUNYCODE, \IDNA_ERROR_PUNYCODE & $info['errors']);
}
}

/**
* Labels above the DNS limit but below the decoder bound still decode:
* ToUnicode reports their decoded form even though they are invalid.
*/
public function testOverLongButDecodableAceLabelStillDecodes()
{
$input = 'example.xn--'.str_repeat('w', 101).'.de';

idn_to_utf8($input, \IDNA_DEFAULT, \INTL_IDNA_VARIANT_UTS46, $info);
$this->assertStringContainsString('example.', $info['result']);
$this->assertStringNotContainsString('xn--', $info['result']);
}

/**
* UTS #46 revision 33: a label starting with "xn--" whose Punycode payload decodes
* to an empty string or to a string containing only ASCII code points must be rejected.
Expand Down
Loading