CGI for htpasswd registration I decided to go with a very simple CGI user registration program to pair with Apache's mod_auth_form wrapper around a private site. It was only just after I finished writing that I realized I'd written security-sensitive code in a language I'm not my best at.

Is this code secure from parameter injection attacks? I.e., if the attacker fiddles with the POST values, can they run arbitrary commands on the server as the web server user?

#!/bin/bash

echo "Content-type: text/html"
echo ''

cat &lt;&lt;EOT
&lt;!doctype html&gt;
&lt;html&gt;
        &lt;head&gt;
                &lt;title&gt;Registered&lt;/title&gt;
                &lt;link rel="icon" href="/favicon.png"&gt;
        &lt;/head&gt;
        &lt;body&gt;
EOT

POST=$(cat)
if [ "$REQUEST_METHOD" != "POST" ] || [ ! "$POST" ]; then
        echo "&lt;h1&gt;Error&lt;/h1&gt;&lt;p&gt;Please go back and try again&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;";
        exit 1;
fi

function input {
        echo "$POST" | sed 's/^.*'"$1"'=\([^&amp;]\+\)&amp;.*$/\1/' | python -c "import sys, urllib.parse as p; print(p.unquote(sys.stdin.read()));"
}

USERNAME=$(input username)
PASSWORD=$(input password)
PASSWORD2=$(input password2)

if [ "$PASSWORD" != "$PASSWORD2" ]; then
        echo "&lt;h1&gt;Error&lt;/h1&gt;&lt;p&gt;Your passwords do not match.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;";
        exit 1;
fi

if [ ${#PASSWORD} -lt 10 ]; then
        echo "&lt;h1&gt;Error&lt;/h1&gt;&lt;p&gt;Please pick a longer password.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;";
        exit 1;
fi

echo "$PASSWORD" | htpasswd -i ../registrations "$USERNAME"

cat &lt;&lt;EOT
                &lt;h1&gt;You have registered&lt;/h1&gt;
                &lt;p&gt;Now, please email me so I can enable your account.&lt;/p&gt;
        &lt;/body&gt;
&lt;/html&gt;
EOT

exit 0


To clarify - I'm not concerned about the line of python - it urldecodes/unescapes the field from the POST data and is extremely battle-tested. I put that in to avoid bugs/security concerns over other solutions with fewer dependencies (many with scary-looking bash constructs).

The sed is also pretty straightforward - it pulls out the named parameter from the urlencoded POST data. &amp; is escaped as %26 and so will never show up in the input. I wrote/modified the sed there and am confident enough that it'll behave as I expect (sed is more familiar to me than bash).

Essentially, please feel free to ignore the implementation of the input function and assume it properly extracts POST variables into bash variables. My concerns center entirely around whether that user supplied data (which can be essentially any sequence of bytes) in bash variables will break or escape from any of the places I use it.
It's a couple of decades (!) since I did any CGI, but I'll cast a quick eye over this.  I hope other reviewers will pitch in and fill the gaps (or correct my errors).

It's an interesting choice to use the same TITLE for success and failure.  It might be better to write a small function:

header()
{
cat &lt;&lt;EOT
Content-type: text/html; charset=us-ascii

&lt;!doctype html&gt;
&lt;html&gt;
        &lt;head&gt;
                &lt;title&gt;$*&lt;/title&gt;
                &lt;link rel="icon" href="/favicon.png"&gt;
        &lt;/head&gt;
        &lt;body&gt;
EOT
}


Note also the form of function declaration - that's more portable than the function keyword, and to be preferred.  We now use it like

if [ "$REQUEST_METHOD" != "POST" ] || [ ! "$POST" ]
then
    header 'Error'
    echo '&lt;h1&gt;Error&lt;/h1&gt;&lt;p&gt;Wrong HTTP method; please go back and try again&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;';
    exit 1;
fi


Also, for that exit, I'd exit 0 there, because the script has succeeded in its job (to report the status to the user).  There's no error as far as the caller (the Web server) is concerned - everything is working correctly.

A further refinement: if we add another cat to the end of header(), it can write the message, too:

#renamed to reflect new purpose
write_html()
{
cat &lt;&lt;EOT
Content-type: text/html; charset=us-ascii

&lt;!doctype html&gt;
&lt;html&gt;
        &lt;head&gt;
                &lt;title&gt;$*&lt;/title&gt;
                &lt;link rel="icon" href="/favicon.png"&gt;
        &lt;/head&gt;
        &lt;body&gt;
        &lt;h1&gt;$*&lt;/h1&gt;
EOT
cat
}

if [ "$REQUEST_METHOD" != "POST" ] || [ ! "$POST" ]
then
    write_html 'Error' &lt;&lt;"EOT"
        &lt;p&gt;Wrong HTTP method; please go back and try again&lt;/p&gt;
    &lt;/body&gt;&lt;/html&gt;
EOT
fi




There seems to be a couple of errors in the sed command in input():

s/^.*$1=\([^&amp;]\+\)&amp;.*$/\1/


The most obvious is the requirement for &amp; after the target parameter - this means we'll be unable to extract the last parameter in the line.  The more subtle bug is that we can be confused if one parameter is a suffix of another, because we didn't anchor the beginning of $1 at all.  Finally, if $1 isn't present in the string, we'll use the entire request as value; we can fix that by printing only if there's a substitution:

# untested
sed -n 's/^.*[?&amp;]'"$1"'=\([^&amp;]\+\).*$/\1/p'


Here, I rely on the greediness of + to take us to the next &amp; or end of line.

Do have another look through the Python library to see if there's something there to help us unpack POST data - since we're using Python anyway for decoding characters, there's no penalty to adding code there.

Since we're using Bash, we could write sed &lt;&lt;&lt;"$POST" instead of echo in that function.

BTW, good job getting the quoting right there!



From here onwards, nothing I haven't already mentioned.