Um punhado de caracteres não é texto para o shell: eles separam comandos, encanam saída e rodam comandos dentro de comandos.
O shell trata alguns caracteres como pontuação com poder, não como letras. Conhecê-los é saber o que uma injeção pode fazer e por que um filtro ingênuo não segura:
; separa comandos: a ; b roda a e depois b.
| encana a saída de um comando para a entrada do próximo: a | b.
&& e || encadeiam por resultado: a && b só roda b se a deu certo.
$(cmd) e crases rodam cmd e colocam a saída ali no meio da linha (substituição de comando).
> e < redirecionam para arquivo; & joga para segundo plano.
${IFS} é a variável com os separadores de campo (espaço, tab, nova linha): serve de espaço onde o espaço foi proibido.
Por isso filtrar "os caracteres ruins" é uma lista de bloqueio que sempre deixa um de fora. Bloquearam o ; e o espaço? Sobra |, $() e ${IFS}. No exemplo abaixo, um filtro remove ; e o espaço, e o atacante passa assim mesmo com | e ${IFS}.
Um filtro apaga o ponto e vírgula e o espaço do texto, mas o pipe e a palavra IFS passam e ainda encadeiam um segundo comando.