CGI for htpasswd registration
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.