Perl, unlike most other languages, uses arrays that are not declared with a particular length and that may grow and shrink in size as is required by subsequent code. In fact, when assigning a value to an element within the array, if the index provided is beyond the end of the array, the array grows to make it valid. Consider the following example:

my @array = (1, 2, 3);            # array initialized
print "Array size is $#array\n";  # 2 (index of last element)
$array[5] = 0;                    # array grows so that reference is valid
print "Array size is $#array\n";  # 5
my $value = $array[7];            # array unchanged + uninitialized value warning
$value = $array[-7];              # array unchanged + uninitialized value warning
if (exists $array[9]) {           # false, array unchanged
  print "That's a big array.\n";
}
print "Array size is $#array\n";  # still 5
$value = $array[10][0];           # reading a value in list context grows array
print "Array size is $#array\n";  # 10!

This automatic growth occurs only if the index provided is positive and the array value is being written, not read, and not passed to a testing function like exists() or defined().

If an attacker is able to substitute a number to be used as an array index and provides the value 1000000000 (1 billion), then Perl will happily try to grow the array to 1 billion elements. Depending on the platform's capabilities, the attempt to grow the array might fail, or hang, or simply cause Perl to consume several gigabytes of memory for the lifetime of the array. Because a consequent denial of service could occur, attackers must not be permitted to control array indices.

Noncompliant Code Example

This noncompliant code example takes a set of users via standard input and adds them to an array, indexed by their UIDs. This program may, for instance, be fed the contents of the /etc/passwd file.

my @users;

while (<STDIN>) {
  my ($username, $dummy, $uid) = split( /:/);
  if (not (defined( $uid) and defined( $username))) {next;}
  if (not $uid =~ /^\d*$/) {next;}
  $users[$uid] = $username;
}

# ... Work with @users

This code clearly skips input lines that do not contain a valid UID or user name. It also skips lines where the UID is not a positive number. However, a UID that is large might cause excessive growth of the @users array and provoke a denial of service.

Compliant Solution

This compliant solution enforces a limit on how large a UID may be. Consequently, the array may not contain more than $max_uid elements.

my @users;
my $max_uid = 10000;

while (<STDIN>) {
  my ($username, $dummy, $uid) = split( /:/);
  if (not (defined( $uid) and defined( $username))) {next;}
  if (not $uid =~ /^\d*$/) {next;}
  if ($uid > $max_uid) {next;}
  $users[$uid] = $username;
}

# ... Work with @users

Risk Assessment

Using unsanitized array index values may exhaust memory and cause the program to terminate or hang.

Recommendation

Severity

Likelihood

Remediation Cost

Priority

Level

IDS32-PL

low

likely

high

P3

L3

Bibliography

 

 


4 Comments

  1. Yves orton says, via the SEI blog entry:

    Regarding the rules of creating elements in an array: perils rules for autovivification revolve around distinguishing between lvalue and r value context. Only in lvalue context are items created. The examples you describe differ in terms of context due to things like aliasing of hash items.

    1. Here's a related question: is 'lvalue' an official term in Perl, like it is in C/C++/Java? The Perl documentation does not use it (except for the new feature 'lvalue subroutines'. We try to use official terms in the standard, as that helps maintain clarity and precision of speech. Offhand, I'm not sure that using 'lvalue' informally to try to explain the circumstances in which arrays grow is not immediately helpful,

  2. I think that the following lines of code might be confusing for some:

     
    while (<STDIN>) {
      my ($username, $dummy, $uid) = split( /:/);
     
    Because though to any experienced Perl programmer the use of $_ is quite obvious, someone who is looking at this documentation may not really understand what's going on with split( /:/). I think this should be considered a little more expansion into something like:
     
    while (my $line = <STDIN>) {
       my ($username, $dummy, $uid) = split /:/, $line;
        ...
    }
     
    Also, quite unimportant but I would always chomp() before I split on standard input if I'm going to be processing all elements of the array the same way, since otherwise you'll have one element with an ugly \n at the end and all the others won't.
    1. We considered eschewing usage of $_.  But it is quite safe to use, in most contexts (see EXP34-PL for one example of insecure usage of $_ ).

       

      While chomp() is a good practice when dissecting a line of text, it is not necessary here, as your typical passwd file will have more than 3 fields.