diff --git a/ShellCheck/Analytics.hs b/ShellCheck/Analytics.hs index b555f90..77206e9 100644 --- a/ShellCheck/Analytics.hs +++ b/ShellCheck/Analytics.hs @@ -141,7 +141,6 @@ nodeChecks = [ ,checkWrongArithmeticAssignment ,checkConditionalAndOrs ,checkFunctionDeclarations - ,checkCatastrophicRm ,checkStderrPipe ,checkOverridingPath ,checkArrayAsString @@ -2135,72 +2134,6 @@ checkFunctionDeclarations params checkFunctionDeclarations _ _ = return () -prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2" -prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo" -prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*" -prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*" -prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*" -prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*" -prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home" -prop_checkCatastrophicRm9 = verifyNot checkCatastrophicRm "rm -rf -- /home" -prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}" -prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec" -prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec" -prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec" -prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg" -prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*" -checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" = - when (any isRecursiveFlag simpleArgs) $ - mapM_ (mapM_ checkWord . braceExpand) tokens - where - simpleArgs = oversimplify t - - checkWord token = - case getLiteralString token of - Just str -> - when (notElem "--" simpleArgs && (fixPath str `elem` importantPaths)) $ - warn (getId token) 2114 "Warning: deletes a system directory. Use 'rm --' to disable this message." - Nothing -> - checkWord' token - - checkWord' token = fromMaybe (return ()) $ do - filename <- getPotentialPath token - let path = fixPath filename - return . when (path `elem` importantPaths) $ - warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ." - - fixPath filename = - let normalized = skipRepeating '/' . skipRepeating '*' $ filename in - if normalized == "/" then normalized else stripTrailing '/' normalized - - getPotentialPath = getLiteralStringExt f - where - f (T_Glob _ str) = return str - f (T_DollarBraced _ word) = - let var = onlyLiteralString word in - if any (`isInfixOf` var) [":?", ":-", ":="] - then Nothing - else return "" - f _ = return "" - - isRecursiveFlag "--recursive" = True - isRecursiveFlag ('-':'-':_) = False - isRecursiveFlag ('-':str) = 'r' `elem` str || 'R' `elem` str - isRecursiveFlag _ = False - - stripTrailing c = reverse . dropWhile (== c) . reverse - skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest) - skipRepeating c (a:r) = a:skipRepeating c r - skipRepeating _ [] = [] - - paths = [ - "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local", - "/var", "/lib" - ] - importantPaths = filter (not . null) $ - ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths) -checkCatastrophicRm _ _ = return () - prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar" prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar" diff --git a/ShellCheck/Checks/Commands.hs b/ShellCheck/Checks/Commands.hs index 4a17f69..80ffed9 100644 --- a/ShellCheck/Checks/Commands.hs +++ b/ShellCheck/Checks/Commands.hs @@ -86,6 +86,7 @@ commandChecks = [ ,checkDeprecatedEgrep ,checkDeprecatedFgrep ,checkWhileGetoptsCase + ,checkCatastrophicRm ] buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) @@ -756,6 +757,65 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f T_Redirecting _ _ x@(T_CaseExpression {}) -> return x _ -> Nothing +prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2" +prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo" +prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*" +prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*" +prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*" +prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*" +prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home" +prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}" +prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec" +prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec" +prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec" +prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg" +prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*" +checkCatastrophicRm = CommandCheck (Basename "rm") $ \t -> + when (isRecursive t) $ + mapM_ (mapM_ checkWord . braceExpand) $ arguments t + where + isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags + + checkWord token = + case getLiteralString token of + Just str -> + when (fixPath str `elem` importantPaths) $ + warn (getId token) 2114 "Warning: deletes a system directory." + Nothing -> + checkWord' token + + checkWord' token = fromMaybe (return ()) $ do + filename <- getPotentialPath token + let path = fixPath filename + return . when (path `elem` importantPaths) $ + warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ." + + fixPath filename = + let normalized = skipRepeating '/' . skipRepeating '*' $ filename in + if normalized == "/" then normalized else stripTrailing '/' normalized + + getPotentialPath = getLiteralStringExt f + where + f (T_Glob _ str) = return str + f (T_DollarBraced _ word) = + let var = onlyLiteralString word in + -- This shouldn't handle non-colon cases. + if any (`isInfixOf` var) [":?", ":-", ":="] + then Nothing + else return "" + f _ = return "" + + stripTrailing c = reverse . dropWhile (== c) . reverse + skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest) + skipRepeating c (a:r) = a:skipRepeating c r + skipRepeating _ [] = [] + + paths = [ + "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local", + "/var", "/lib", "/dev", "/media", "/boot", "/lib64", "/usr/bin" + ] + importantPaths = filter (not . null) $ + ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths) return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])