packages feed

wai-app-file-cgi 3.2.3 → 3.3.0

raw patch · 9 files changed

+138/−12 lines, 9 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,19 @@ # ChangeLog for wai-app-file-cgi +## 3.3.0++- **Breaking**: `rawPathInfo` is percent-decoded in `fileApp` and+  `cgiApp`, including `PATH_INFO` and `SCRIPT_NAME`. Callers that+  decode it beforehand (e.g. mighttpd2 up to now) must pass the+  original request; otherwise paths containing `%` are decoded+  twice.+- **Breaking**: paths that do not match the source of a route are+  rejected with 404 in all applications. A source with a trailing+  slash also matches the path without it.+- **Breaking**: paths containing a segment starting with `.` (e.g.+  `.git`), NUL, or encoded `..` are rejected with 400 in `fileApp`+  and `cgiApp`. `/.well-known/` is allowed.+ ## 3.2.3  - Security: request headers containing `_` are not passed to CGI
Network/Wai/Application/Classic/CGI.hs view
@@ -22,6 +22,7 @@ import Network.Wai.Conduit import Network.Wai.Application.Classic.Conduit import Network.Wai.Application.Classic.Field+import Network.Wai.Application.Classic.FileInfo (decodedPathInfo, isUnsafePath, matchRoute) import Network.Wai.Application.Classic.Header import Network.Wai.Application.Classic.Path import Network.Wai.Application.Classic.Types@@ -45,24 +46,28 @@ {-|   Handle any methods for CGI. +Paths containing a segment starting with \'.\' are rejected with 400.+ CGI processes are reaped by this library. The program to link this library should not ignore SIGCHLD. Otherwise, the PID of a finished CGI process may be reused by another process before it is terminated. -} cgiApp :: ClassicAppSpec -> CgiAppSpec -> CgiRoute -> Application cgiApp cspec spec cgii req respond-  | hasDotDotSegment (rawPathInfo req) =+  | isUnsafePath (rawPathInfo req) =       respond $ responseLBS badRequest400 textPlainHeader "Bad Request\r\n"+  | not (matchRoute (cgiSrc cgii) (decodedPathInfo req)) = notFound   | otherwise = do       exist <- doesFileExist prog       if exist           then E.bracket setup teardown cgi-          else respond $ responseLBS notFound404 textPlainHeader "Not Found\r\n"+          else notFound   where+    notFound = respond $ responseLBS notFound404 textPlainHeader "Not Found\r\n"     cgiPath@(prog,_,_) =         pathinfoToCGI (cgiSrc cgii)                       (cgiDst cgii)-                      (rawPathInfo req)+                      (decodedPathInfo req)                       (indexCgi spec)     setup = execProcess cspec cgiPath req     -- An unreaped process keeps its PID, so SIGTERM cannot hit
Network/Wai/Application/Classic/File.hs view
@@ -50,6 +50,9 @@ -- If 'rawPathInfo' does not end with \'/\' and a corresponding index file -- exist, redirection is specified in HTTP response. --+-- Paths containing a segment starting with \'.\' (e.g. \".git\")+-- are rejected with 400, except \"\/.well-known\/\".+-- -- Directory contents are NOT automatically listed. To list directory -- contents, an index file must be created beforehand. --@@ -58,7 +61,8 @@ fileApp :: ClassicAppSpec -> FileAppSpec -> FileRoute -> Application fileApp cspec spec filei req respond = do     rspspec <- case method of-        _ | hasDotDotSegment (rawPathInfo req) -> return badRequest+        _ | isUnsafePath (rawPathInfo req) -> return badRequest+        _ | not (matchRoute (fileSrc filei) (decodedPathInfo req)) -> return notFound         Right GET -> processGET hinfo ishtml rfile         Right HEAD -> processGET hinfo ishtml rfile         _ -> return notAllowed
Network/Wai/Application/Classic/FileInfo.hs view
@@ -1,6 +1,9 @@+{-# LANGUAGE OverloadedStrings #-}+ module Network.Wai.Application.Classic.FileInfo where  import qualified Data.ByteString as BS+import Network.HTTP.Types (urlDecode) import Network.Wai import Network.Wai.Application.Classic.Path import Network.Wai.Application.Classic.Types@@ -37,15 +40,65 @@ -- "/tmp/dst/" -- >>> pathinfoToFilePath (mkReq "/exact/match/more.html") (FileRoute "/exact/match" "/tmp/dst") -- "/tmp/dst/more.html"+-- >>> pathinfoToFilePath (mkReq "/no%5Fext%20ension") (FileRoute "/" "/srv/http/")+-- "/srv/http/no_ext ension" pathinfoToFilePath :: Request -> FileRoute -> Path pathinfoToFilePath req filei     | BS.null path' = dst     | otherwise = dst </> path'   where-    path = rawPathInfo req+    path = decodedPathInfo req     src = fileSrc filei     dst = fileDst filei     path' = path <\> src++-- | 'rawPathInfo' with percent-encoding decoded.+decodedPathInfo :: Request -> Path+decodedPathInfo = urlDecode False . rawPathInfo++-- | Whether the path is routed by the source of a route.+--   A source with a trailing slash also matches the path without it.+--+-- >>> matchRoute "/static/" "/static/a.html"+-- True+-- >>> matchRoute "/static/" "/static"+-- True+-- >>> matchRoute "/static/" "/other/static/a.html"+-- False+-- >>> matchRoute "/exact/match" "/exact/match/more.html"+-- True+matchRoute :: Path -> Path -> Bool+matchRoute src path =+    src `BS.isPrefixOf` path+        || hasTrailingPathSeparator src && path == BS.init src++-- | Whether the path, once decoded, contains NUL or a segment+--   starting with \'.\' (e.g. ".." and ".git").+--   \"\/.well-known\/\" is allowed for ACME.+--+-- >>> isUnsafePath "/foo/%2e%2e/bar"+-- True+-- >>> isUnsafePath "/foo%00.html"+-- True+-- >>> isUnsafePath "/foo/..bar"+-- True+-- >>> isUnsafePath "/%2egit/config"+-- True+-- >>> isUnsafePath "/foo.d/bar.html"+-- False+-- >>> isUnsafePath "/.well-known/acme-challenge/x"+-- False+-- >>> isUnsafePath "/.well-known/.git/config"+-- True+isUnsafePath :: Path -> Bool+isUnsafePath raw = BS.elem 0 path || any isHidden segs'+  where+    path = urlDecode False raw+    segs = BS.splitWith (\w -> w == 47 || w == 92) path -- '/' and '\\'+    segs' = case segs of+        "" : ".well-known" : rest -> rest+        _ -> segs+    isHidden seg = "." `BS.isPrefixOf` seg  -- | --
Network/Wai/Application/Classic/Redirect.hs view
@@ -8,12 +8,14 @@ import Network.HTTP.Types import Network.Wai import Network.Wai.Application.Classic.Field+import Network.Wai.Application.Classic.FileInfo (matchRoute) import Network.Wai.Application.Classic.Path import Network.Wai.Application.Classic.Types  redirectApp :: ClassicAppSpec -> RedirectRoute -> Application-redirectApp _ route req respond =-    respond $ responseLBS status hdr ""+redirectApp _ route req respond+    | matchRoute src path = respond $ responseLBS status hdr ""+    | otherwise = respond $ responseLBS notFound404 textPlainHeader "Not Found\r\n"   where     path = rawPathInfo req     src = redirectSrc route
Network/Wai/Application/Classic/RevProxy.hs view
@@ -17,6 +17,7 @@ import Network.Wai.Application.Classic.Conduit import Network.Wai.Application.Classic.EventSource import Network.Wai.Application.Classic.Field+import Network.Wai.Application.Classic.FileInfo (matchRoute) import Network.Wai.Application.Classic.Path import Network.Wai.Application.Classic.Types import Network.Wai.Conduit@@ -28,10 +29,13 @@  -- |  Relaying any requests as reverse proxy. revProxyApp :: ClassicAppSpec -> RevProxyAppSpec -> RevProxyRoute -> Application-revProxyApp cspec spec route req respond = do-    responded <- newIORef False-    H.withResponse httpClientRequest mgr (proxy responded)-        `E.catch` badGateway responded+revProxyApp cspec spec route req respond+    | not (matchRoute (revProxySrc route) (rawPathInfo req)) =+        respond $ responseLBS notFound404 textPlainHeader "Not Found\r\n"+    | otherwise = do+        responded <- newIORef False+        H.withResponse httpClientRequest mgr (proxy responded)+            `E.catch` badGateway responded   where     proxy responded hrsp = do         let status = H.responseStatus hrsp
test/ClassicSpec.hs view
@@ -18,6 +18,10 @@             let url = "http://127.0.0.1:2345/cgi-bin/"             bdy <- rspBody <$> sendGET url             bdy `shouldBe` "index.cgi\n"+        it "decodes PATH_INFO" $ do+            let url = "http://127.0.0.1:2345/cgi-bin/index.cgi/a%20b"+            bdy <- rspBody <$> sendGET url+            bdy `shouldBe` "PATH_INFO: /a b\n"         it "accepts POST" $ do             let url = "http://127.0.0.1:2345/cgi-bin/echo-env/pathinfo?query=foo"             bdy <- rspBody <$> sendPOST url "foo bar.\nbaz!\n"@@ -61,6 +65,10 @@             let url = "http://127.0.0.1:2345/cgi-bin/broken"             sc <- rspCode <$> sendPOST url "foo bar.\nbaz!\n"             sc `shouldBe` (5, 0, 0)+        it "returns 400 for hidden files" $ do+            let url = "http://127.0.0.1:2345/cgi-bin/%2ehidden"+            sc <- rspCode <$> sendGET url+            sc `shouldBe` (4, 0, 0)         it "returns 404 if the CGI does not exist" $ do             let url = "http://127.0.0.1:2345/cgi-bin/nonexistent"             sc <- rspCode <$> sendGET url@@ -89,6 +97,12 @@             bdy <- bracket_ (writeFile file "hello\n") (removeFile file) $                 H.responseBody <$> H.httpLbs hreq' mgr             bdy `shouldBe` "hello\n"+        it "serves files with percent-encoded non-ASCII names" $ do+            let url = "http://127.0.0.1:2345/%E6%97%A5%E6%9C%AC.txt"+                file = "test/html/\26085\26412.txt"+            bdy <- bracket_ (writeFile file "hello\n") (removeFile file) $+                rspBody <$> sendGET url+            bdy `shouldBe` "hello\n"         it "returns 404 if not exist" $ do             let url = "http://127.0.0.1:2345/dummy"             sc <- rspCode <$> sendGET url@@ -97,6 +111,23 @@             let url = "http://127.0.0.1:2345/../../../../../../etc/hosts"             sc <- rspCode <$> sendGET url             sc `shouldBe` (4, 0, 0)+        it "returns 400 if the path contains encoded .." $ do+            let url = "http://127.0.0.1:2345/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/hosts"+            sc <- rspCode <$> sendGET url+            sc `shouldBe` (4, 0, 0)+        it "returns 400 for hidden files even if encoded" $ do+            sc1 <- rspCode <$> sendGET "http://127.0.0.1:2345/.git/config"+            sc1 `shouldBe` (4, 0, 0)+            sc2 <- rspCode <$> sendGET "http://127.0.0.1:2345/%2egit/config"+            sc2 `shouldBe` (4, 0, 0)+        it "does not reject /.well-known/" $ do+            sc <- rspCode <$> sendGET "http://127.0.0.1:2345/.well-known/acme-challenge/x"+            sc `shouldBe` (4, 0, 4)+        it "decodes percent-encoded paths" $ do+            let url = "http://127.0.0.1:2345/no%5Fextension"+            bdy <- rspBody <$> sendGET url+            ans <- readFileAscii "test/html/no_extension"+            bdy `shouldBe` ans         it "returns Japanese HTML if language is specified" $ do             let url = "http://127.0.0.1:2345/ja/"             bdy <- rspBody <$> sendGETwH url [Header HdrAcceptLanguage "ja, en;q=0.7"]@@ -156,6 +187,11 @@             rsp <- sendGET url             lookupHeader HdrLocation (rspHeaders rsp)                 `shouldBe` Just "//127.0.0.1:2345/redirect/?foo=bar"+        it "returns 404 if the path does not match the route" $ do+            -- "/mismatch" has the same length as "/statics/".+            let url = "http://127.0.0.1:2345/mismatchindex.html"+            sc <- rspCode <$> sendGET url+            sc `shouldBe` (4, 0, 4)         it "returns index.html for /exact-route.html matching exactRoute" $ do             let url = "http://127.0.0.1:2345/exact-route.html"             bdy <- rspBody <$> sendGET url
test/Spec.hs view
@@ -50,6 +50,7 @@     | upstream = revProxyApp appSpec (RevProxyAppSpec mgr) upstreamRoute req     | redirect = redirectApp appSpec redirectRoute req     | cgi = cgiApp appSpec defaultCgiAppSpec cgiRoute req+    | mismatch = fileApp appSpec defaultFileAppSpec mismatchRoute req     | exact = fileApp appSpec defaultFileAppSpec exactRoute req     | otherwise = fileApp appSpec defaultFileAppSpec fileRoute req   where@@ -57,6 +58,7 @@     upstream = "/upstream/" `BS.isPrefixOf` rawPathInfo req     redirect = "/redirect-app/" `BS.isPrefixOf` rawPathInfo req     cgi = "/cgi-bin/" `BS.isPrefixOf` rawPathInfo req+    mismatch = "/mismatch" `BS.isPrefixOf` rawPathInfo req     exact = "/exact-route.html" `BS.isPrefixOf` rawPathInfo req     appSpec = defaultClassicAppSpec{softwareName = "ClassicTester"}     redirectRoute =@@ -73,6 +75,12 @@         FileRoute             { fileSrc = "/exact-route.html"             , fileDst = fromString (dir </> "test/html/index.html")+            }+    -- Dispatched although the source does not match.+    mismatchRoute =+        FileRoute+            { fileSrc = "/statics/"+            , fileDst = fromString (dir </> "test/html/")             }     fileRoute =         FileRoute
wai-app-file-cgi.cabal view
@@ -1,6 +1,6 @@ cabal-version:      1.18 name:               wai-app-file-cgi-version:            3.2.3+version:            3.3.0 license:            BSD3 license-file:       LICENSE maintainer:         Kazu Yamamoto <kazu@iij.ad.jp>