Merge pull request #1455 from yesodweb/csrfBetterErrors
Give better error messages when CSRF validation fails
This commit is contained in:
commit
c81ad91cd1
@ -1,5 +1,10 @@
|
|||||||
|
## 1.4.37.2
|
||||||
|
|
||||||
|
* Improve error messages for the CSRF checking functions [#1455](https://github.com/yesodweb/yesod/issues/1455)
|
||||||
|
|
||||||
## 1.4.37.1
|
## 1.4.37.1
|
||||||
* Fix documentation on `languages` function, update `getMessageRender` to use said function. [#1457] (https://github.com/yesodweb/yesod/pull/1457)
|
|
||||||
|
* Fix documentation on `languages` function, update `getMessageRender` to use said function. [#1457](https://github.com/yesodweb/yesod/pull/1457)
|
||||||
|
|
||||||
## 1.4.37
|
## 1.4.37
|
||||||
|
|
||||||
|
|||||||
@ -242,7 +242,7 @@ import Yesod.Core.Types
|
|||||||
import Yesod.Routes.Class (Route)
|
import Yesod.Routes.Class (Route)
|
||||||
import Blaze.ByteString.Builder (Builder)
|
import Blaze.ByteString.Builder (Builder)
|
||||||
import Safe (headMay)
|
import Safe (headMay)
|
||||||
import Data.CaseInsensitive (CI)
|
import Data.CaseInsensitive (CI, original)
|
||||||
import qualified Data.Conduit.List as CL
|
import qualified Data.Conduit.List as CL
|
||||||
import Control.Monad.Trans.Resource (MonadResource, InternalState, runResourceT, withInternalState, getInternalState, liftResourceT, resourceForkIO)
|
import Control.Monad.Trans.Resource (MonadResource, InternalState, runResourceT, withInternalState, getInternalState, liftResourceT, resourceForkIO)
|
||||||
import qualified System.PosixCompat.Files as PC
|
import qualified System.PosixCompat.Files as PC
|
||||||
@ -1501,18 +1501,22 @@ defaultCsrfHeaderName = "X-XSRF-TOKEN"
|
|||||||
-- @since 1.4.14
|
-- @since 1.4.14
|
||||||
checkCsrfHeaderNamed :: MonadHandler m => CI S8.ByteString -> m ()
|
checkCsrfHeaderNamed :: MonadHandler m => CI S8.ByteString -> m ()
|
||||||
checkCsrfHeaderNamed headerName = do
|
checkCsrfHeaderNamed headerName = do
|
||||||
valid <- hasValidCsrfHeaderNamed headerName
|
(valid, mHeader) <- hasValidCsrfHeaderNamed' headerName
|
||||||
unless valid (permissionDenied csrfErrorMessage)
|
unless valid (permissionDenied $ csrfErrorMessage [CSRFHeader (decodeUtf8 $ original headerName) mHeader])
|
||||||
|
|
||||||
-- | Takes a header name to lookup a CSRF token, and returns whether the value matches the token stored in the session.
|
-- | Takes a header name to lookup a CSRF token, and returns whether the value matches the token stored in the session.
|
||||||
--
|
--
|
||||||
-- @since 1.4.14
|
-- @since 1.4.14
|
||||||
hasValidCsrfHeaderNamed :: MonadHandler m => CI S8.ByteString -> m Bool
|
hasValidCsrfHeaderNamed :: MonadHandler m => CI S8.ByteString -> m Bool
|
||||||
hasValidCsrfHeaderNamed headerName = do
|
hasValidCsrfHeaderNamed headerName = fst <$> hasValidCsrfHeaderNamed' headerName
|
||||||
|
|
||||||
|
-- | Like 'hasValidCsrfHeaderNamed', but also returns the header value to be used in error messages.
|
||||||
|
hasValidCsrfHeaderNamed' :: MonadHandler m => CI S8.ByteString -> m (Bool, Maybe Text)
|
||||||
|
hasValidCsrfHeaderNamed' headerName = do
|
||||||
mCsrfToken <- reqToken <$> getRequest
|
mCsrfToken <- reqToken <$> getRequest
|
||||||
mXsrfHeader <- lookupHeader headerName
|
mXsrfHeader <- lookupHeader headerName
|
||||||
|
|
||||||
return $ validCsrf mCsrfToken mXsrfHeader
|
return $ (validCsrf mCsrfToken mXsrfHeader, decodeUtf8 <$> mXsrfHeader)
|
||||||
|
|
||||||
-- CSRF Parameter checking
|
-- CSRF Parameter checking
|
||||||
|
|
||||||
@ -1528,18 +1532,22 @@ defaultCsrfParamName = "_token"
|
|||||||
-- @since 1.4.14
|
-- @since 1.4.14
|
||||||
checkCsrfParamNamed :: MonadHandler m => Text -> m ()
|
checkCsrfParamNamed :: MonadHandler m => Text -> m ()
|
||||||
checkCsrfParamNamed paramName = do
|
checkCsrfParamNamed paramName = do
|
||||||
valid <- hasValidCsrfParamNamed paramName
|
(valid, mParam) <- hasValidCsrfParamNamed' paramName
|
||||||
unless valid (permissionDenied csrfErrorMessage)
|
unless valid (permissionDenied $ csrfErrorMessage [CSRFParam paramName mParam])
|
||||||
|
|
||||||
-- | Takes a POST parameter name to lookup a CSRF token, and returns whether the value matches the token stored in the session.
|
-- | Takes a POST parameter name to lookup a CSRF token, and returns whether the value matches the token stored in the session.
|
||||||
--
|
--
|
||||||
-- @since 1.4.14
|
-- @since 1.4.14
|
||||||
hasValidCsrfParamNamed :: MonadHandler m => Text -> m Bool
|
hasValidCsrfParamNamed :: MonadHandler m => Text -> m Bool
|
||||||
hasValidCsrfParamNamed paramName = do
|
hasValidCsrfParamNamed paramName = fst <$> hasValidCsrfParamNamed' paramName
|
||||||
|
|
||||||
|
-- | Like 'hasValidCsrfParamNamed', but also returns the param value to be used in error messages.
|
||||||
|
hasValidCsrfParamNamed' :: MonadHandler m => Text -> m (Bool, Maybe Text)
|
||||||
|
hasValidCsrfParamNamed' paramName = do
|
||||||
mCsrfToken <- reqToken <$> getRequest
|
mCsrfToken <- reqToken <$> getRequest
|
||||||
mCsrfParam <- lookupPostParam paramName
|
mCsrfParam <- lookupPostParam paramName
|
||||||
|
|
||||||
return $ validCsrf mCsrfToken (encodeUtf8 <$> mCsrfParam)
|
return $ (validCsrf mCsrfToken (encodeUtf8 <$> mCsrfParam), mCsrfParam)
|
||||||
|
|
||||||
-- | Checks that a valid CSRF token is present in either the request headers or POST parameters.
|
-- | Checks that a valid CSRF token is present in either the request headers or POST parameters.
|
||||||
-- If the value doesn't match the token stored in the session, this function throws a 'PermissionDenied' error.
|
-- If the value doesn't match the token stored in the session, this function throws a 'PermissionDenied' error.
|
||||||
@ -1550,11 +1558,12 @@ checkCsrfHeaderOrParam :: (MonadHandler m, MonadLogger m)
|
|||||||
-> Text -- ^ The POST parameter name to lookup the CSRF token
|
-> Text -- ^ The POST parameter name to lookup the CSRF token
|
||||||
-> m ()
|
-> m ()
|
||||||
checkCsrfHeaderOrParam headerName paramName = do
|
checkCsrfHeaderOrParam headerName paramName = do
|
||||||
validHeader <- hasValidCsrfHeaderNamed headerName
|
(validHeader, mHeader) <- hasValidCsrfHeaderNamed' headerName
|
||||||
validParam <- hasValidCsrfParamNamed paramName
|
(validParam, mParam) <- hasValidCsrfParamNamed' paramName
|
||||||
unless (validHeader || validParam) $ do
|
unless (validHeader || validParam) $ do
|
||||||
$logWarnS "yesod-core" csrfErrorMessage
|
let errorMessage = csrfErrorMessage $ [CSRFHeader (decodeUtf8 $ original headerName) mHeader, CSRFParam paramName mParam]
|
||||||
permissionDenied csrfErrorMessage
|
$logWarnS "yesod-core" errorMessage
|
||||||
|
permissionDenied errorMessage
|
||||||
|
|
||||||
validCsrf :: Maybe Text -> Maybe S.ByteString -> Bool
|
validCsrf :: Maybe Text -> Maybe S.ByteString -> Bool
|
||||||
-- It's important to use constant-time comparison (constEqBytes) in order to avoid timing attacks.
|
-- It's important to use constant-time comparison (constEqBytes) in order to avoid timing attacks.
|
||||||
@ -1562,5 +1571,25 @@ validCsrf (Just token) (Just param) = encodeUtf8 token `constEqBytes` param
|
|||||||
validCsrf Nothing _param = True
|
validCsrf Nothing _param = True
|
||||||
validCsrf (Just _token) Nothing = False
|
validCsrf (Just _token) Nothing = False
|
||||||
|
|
||||||
csrfErrorMessage :: Text
|
data CSRFExpectation = CSRFHeader Text (Maybe Text) -- Key/Value
|
||||||
csrfErrorMessage = "A valid CSRF token wasn't present in HTTP headers or POST parameters. Because the request could have been forged, it's been rejected altogether. Check the Yesod.Core.Handler docs of the yesod-core package for details on CSRF protection."
|
| CSRFParam Text (Maybe Text) -- Key/Value
|
||||||
|
|
||||||
|
csrfErrorMessage :: [CSRFExpectation]
|
||||||
|
-> Text -- ^ Error message
|
||||||
|
csrfErrorMessage expectedLocations = T.intercalate "\n"
|
||||||
|
[ "A valid CSRF token wasn't present. Because the request could have been forged, it's been rejected altogether."
|
||||||
|
, "If you're a developer of this site, these tips will help you debug the issue:"
|
||||||
|
, "- Read the Yesod.Core.Handler docs of the yesod-core package for details on CSRF protection."
|
||||||
|
, "- Check that your HTTP client is persisting cookies between requests, like a browser does."
|
||||||
|
, "- By default, the CSRF token is sent to the client in a cookie named " `mappend` (decodeUtf8 defaultCsrfCookieName) `mappend` "."
|
||||||
|
, "- The server is looking for the token in the following locations:\n" `mappend` T.intercalate "\n" (map csrfLocation expectedLocations)
|
||||||
|
]
|
||||||
|
|
||||||
|
where csrfLocation expected = case expected of
|
||||||
|
CSRFHeader k v -> T.intercalate " " [" - An HTTP header named", k, (formatValue v)]
|
||||||
|
CSRFParam k v -> T.intercalate " " [" - A POST parameter named", k, (formatValue v)]
|
||||||
|
|
||||||
|
formatValue :: Maybe Text -> Text
|
||||||
|
formatValue maybeText = case maybeText of
|
||||||
|
Nothing -> "(which is not currently set)"
|
||||||
|
Just t -> T.concat ["(which has the current, incorrect value: '", t, "')"]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-core
|
name: yesod-core
|
||||||
version: 1.4.37.1
|
version: 1.4.37.2
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman <michael@snoyman.com>
|
author: Michael Snoyman <michael@snoyman.com>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user