added functions to simplify application of an ssl-only policy to a site
This commit is contained in:
parent
09df930de3
commit
8b7c58f381
@ -47,6 +47,8 @@ module Yesod.Core
|
|||||||
, defaultClientSessionBackend
|
, defaultClientSessionBackend
|
||||||
, envClientSessionBackend
|
, envClientSessionBackend
|
||||||
, clientSessionBackend
|
, clientSessionBackend
|
||||||
|
, sslOnlySessions
|
||||||
|
, sslOnlyMiddleware
|
||||||
, clientSessionDateCacher
|
, clientSessionDateCacher
|
||||||
, loadClientSession
|
, loadClientSession
|
||||||
, Header(..)
|
, Header(..)
|
||||||
|
|||||||
@ -310,6 +310,39 @@ defaultYesodMiddleware handler = do
|
|||||||
authorizationCheck
|
authorizationCheck
|
||||||
handler
|
handler
|
||||||
|
|
||||||
|
-- | Defends against session hijacking by setting the secure bit on session
|
||||||
|
-- cookies so that browsers will not transmit them over http. With this
|
||||||
|
-- setting on, it follows that the server will regard requests made over
|
||||||
|
-- http as sessionless, because the session cookie will not be included in
|
||||||
|
-- the request. Use this as part of a total security measure which also
|
||||||
|
-- includes disabling HTTP traffic to the site or issuing redirects from
|
||||||
|
-- HTTP urls, and composing 'sslOnlyMiddleware' with the site's
|
||||||
|
-- 'yesodMiddleware'.
|
||||||
|
sslOnlySessions :: IO (Maybe SessionBackend) -> IO (Maybe SessionBackend)
|
||||||
|
sslOnlySessions = (fmap . fmap) secureSessionCookies
|
||||||
|
where
|
||||||
|
setSecureBit cookie = cookie { setCookieSecure = True }
|
||||||
|
secureSessionCookies = customizeSessionCookies setSecureBit
|
||||||
|
|
||||||
|
-- | Apply a Strict-Transport-Security header with the specified timeout to
|
||||||
|
-- all responses so that browsers will rewrite all http links to https
|
||||||
|
-- until the timeout expires. For security, the max-age of the STS header
|
||||||
|
-- should always equal or exceed the client sessions timeout. This defends
|
||||||
|
-- against hijacking attacks on the sessions of users who attempt to access
|
||||||
|
-- the site using an http url. This middleware makes a site functionally
|
||||||
|
-- inaccessible over vanilla http in all standard browsers.
|
||||||
|
sslOnlyMiddleware :: Yesod site
|
||||||
|
=> Int -- ^ minutes
|
||||||
|
-> HandlerT site IO res
|
||||||
|
-> HandlerT site IO res
|
||||||
|
sslOnlyMiddleware timeout handler = do
|
||||||
|
addHeader "Strict-Transport-Security"
|
||||||
|
$ T.pack $ concat [ "max-age="
|
||||||
|
, show $ timeout * 60
|
||||||
|
, "; includeSubDomains"
|
||||||
|
]
|
||||||
|
handler
|
||||||
|
|
||||||
-- | Check if a given request is authorized via 'isAuthorized' and
|
-- | Check if a given request is authorized via 'isAuthorized' and
|
||||||
-- 'isWriteRequest'.
|
-- 'isWriteRequest'.
|
||||||
--
|
--
|
||||||
@ -570,7 +603,8 @@ formatLogMessage getdate loc src level msg = do
|
|||||||
-- would work across many subdomains:
|
-- would work across many subdomains:
|
||||||
--
|
--
|
||||||
-- @
|
-- @
|
||||||
-- makeSessionBackend = fmap (customizeSessionCookie addDomain) ...
|
-- makeSessionBackend site =
|
||||||
|
-- (fmap . fmap) (customizeSessionCookies addDomain) ...
|
||||||
-- where
|
-- where
|
||||||
-- addDomain cookie = cookie { 'setCookieDomain' = Just \".example.com\" }
|
-- addDomain cookie = cookie { 'setCookieDomain' = Just \".example.com\" }
|
||||||
-- @
|
-- @
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import qualified YesodCoreTest.Streaming as Streaming
|
|||||||
import qualified YesodCoreTest.Reps as Reps
|
import qualified YesodCoreTest.Reps as Reps
|
||||||
import qualified YesodCoreTest.Auth as Auth
|
import qualified YesodCoreTest.Auth as Auth
|
||||||
import qualified YesodCoreTest.LiteApp as LiteApp
|
import qualified YesodCoreTest.LiteApp as LiteApp
|
||||||
|
import qualified YesodCoreTest.Ssl as Ssl
|
||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
@ -44,3 +45,5 @@ specs = do
|
|||||||
Reps.specs
|
Reps.specs
|
||||||
Auth.specs
|
Auth.specs
|
||||||
LiteApp.specs
|
LiteApp.specs
|
||||||
|
Ssl.unsecSpec
|
||||||
|
Ssl.sslOnlySpec
|
||||||
|
|||||||
64
yesod-core/test/YesodCoreTest/Ssl.hs
Normal file
64
yesod-core/test/YesodCoreTest/Ssl.hs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
|
||||||
|
module YesodCoreTest.Ssl ( sslOnlySpec, unsecSpec ) where
|
||||||
|
import qualified YesodCoreTest.StubSslOnly as Ssl
|
||||||
|
import qualified YesodCoreTest.StubUnsecured as Unsecured
|
||||||
|
import Yesod.Core
|
||||||
|
import Test.Hspec
|
||||||
|
import Network.Wai
|
||||||
|
import Network.Wai.Test
|
||||||
|
import qualified Data.ByteString.Char8 as C8
|
||||||
|
import qualified Web.Cookie as Cookie
|
||||||
|
import qualified Data.List as DL
|
||||||
|
|
||||||
|
type CookieSpec = Cookie.SetCookie -> Bool
|
||||||
|
|
||||||
|
type ResponseExpectation = SResponse -> Session ()
|
||||||
|
|
||||||
|
homeFixtureFor :: YesodDispatch a => a -> ResponseExpectation -> IO ()
|
||||||
|
homeFixtureFor app assertion = do
|
||||||
|
wa <- toWaiApp app
|
||||||
|
runSession (getHome >>= assertion) wa
|
||||||
|
where
|
||||||
|
getHome = request defaultRequest
|
||||||
|
|
||||||
|
cookieShouldSatisfy :: String -> CookieSpec -> ResponseExpectation
|
||||||
|
cookieShouldSatisfy name spec response =
|
||||||
|
liftIO $
|
||||||
|
case DL.filter matchesName $ cookiesIn response of
|
||||||
|
[] -> expectationFailure $ DL.concat
|
||||||
|
[ "Expected a cookie named "
|
||||||
|
, name
|
||||||
|
, " but none is set"
|
||||||
|
]
|
||||||
|
[c] -> c `shouldSatisfy` spec
|
||||||
|
_ -> expectationFailure $ DL.concat
|
||||||
|
[ "Expected one cookie named "
|
||||||
|
, name
|
||||||
|
, " but found more than one"
|
||||||
|
]
|
||||||
|
where
|
||||||
|
matchesName c = (Cookie.setCookieName c) == C8.pack name
|
||||||
|
cookiesIn r =
|
||||||
|
DL.map
|
||||||
|
(Cookie.parseSetCookie . snd)
|
||||||
|
(DL.filter (("Set-Cookie"==) . fst) $ simpleHeaders r)
|
||||||
|
|
||||||
|
sslOnlySpec :: Spec
|
||||||
|
sslOnlySpec = describe "A Yesod application with sslOnly on" $ do
|
||||||
|
it "serves a Strict-Transport-Security header in all responses" $
|
||||||
|
atHome $ assertHeader "Strict-Transport-Security"
|
||||||
|
"max-age=7200; includeSubDomains"
|
||||||
|
it "sets the Secure flag on its session cookie" $
|
||||||
|
atHome $ "_SESSION" `cookieShouldSatisfy` Cookie.setCookieSecure
|
||||||
|
where
|
||||||
|
atHome = homeFixtureFor Ssl.App
|
||||||
|
|
||||||
|
unsecSpec :: Spec
|
||||||
|
unsecSpec = describe "A Yesod application with sslOnly off" $ do
|
||||||
|
it "never serves a Strict-Transport-Security header" $ do
|
||||||
|
atHome $ assertNoHeader "Strict-Transport-Security"
|
||||||
|
it "does not set the Secure flag on its session cookie" $ do
|
||||||
|
atHome $ "_SESSION" `cookieShouldSatisfy` isNotSecure
|
||||||
|
where
|
||||||
|
atHome = homeFixtureFor Unsecured.App
|
||||||
|
isNotSecure c = not $ Cookie.setCookieSecure c
|
||||||
23
yesod-core/test/YesodCoreTest/StubSslOnly.hs
Normal file
23
yesod-core/test/YesodCoreTest/StubSslOnly.hs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
|
||||||
|
module YesodCoreTest.StubSslOnly ( App ( App ) ) where
|
||||||
|
|
||||||
|
import Yesod.Core
|
||||||
|
import qualified Web.ClientSession as CS
|
||||||
|
|
||||||
|
data App = App
|
||||||
|
|
||||||
|
mkYesod "App" [parseRoutes|
|
||||||
|
/ HomeR GET
|
||||||
|
|]
|
||||||
|
|
||||||
|
instance Yesod App where
|
||||||
|
yesodMiddleware = defaultYesodMiddleware . (sslOnlyMiddleware 120)
|
||||||
|
makeSessionBackend _ = sslOnlySessions $
|
||||||
|
fmap Just $ defaultClientSessionBackend 120 CS.defaultKeyFile
|
||||||
|
|
||||||
|
getHomeR :: Handler Html
|
||||||
|
getHomeR = defaultLayout
|
||||||
|
[whamlet|
|
||||||
|
<p>
|
||||||
|
Welcome to my test application.
|
||||||
|
|]
|
||||||
19
yesod-core/test/YesodCoreTest/StubUnsecured.hs
Normal file
19
yesod-core/test/YesodCoreTest/StubUnsecured.hs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
|
||||||
|
module YesodCoreTest.StubUnsecured ( App ( App ) ) where
|
||||||
|
|
||||||
|
import Yesod.Core
|
||||||
|
|
||||||
|
data App = App
|
||||||
|
|
||||||
|
mkYesod "App" [parseRoutes|
|
||||||
|
/ HomeR GET
|
||||||
|
|]
|
||||||
|
|
||||||
|
instance Yesod App
|
||||||
|
|
||||||
|
getHomeR :: Handler Html
|
||||||
|
getHomeR = defaultLayout
|
||||||
|
[whamlet|
|
||||||
|
<p>
|
||||||
|
Welcome to my test application.
|
||||||
|
|]
|
||||||
@ -138,6 +138,8 @@ test-suite tests
|
|||||||
cpp-options: -DTEST
|
cpp-options: -DTEST
|
||||||
build-depends: base
|
build-depends: base
|
||||||
,hspec >= 1.3
|
,hspec >= 1.3
|
||||||
|
,hspec-expectations
|
||||||
|
,clientsession
|
||||||
,wai >= 3.0
|
,wai >= 3.0
|
||||||
,yesod-core
|
,yesod-core
|
||||||
,bytestring
|
,bytestring
|
||||||
@ -159,6 +161,7 @@ test-suite tests
|
|||||||
, streaming-commons
|
, streaming-commons
|
||||||
, wai-extra
|
, wai-extra
|
||||||
, mwc-random
|
, mwc-random
|
||||||
|
, cookie >= 0.4.1 && < 0.5
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
extensions: TemplateHaskell
|
extensions: TemplateHaskell
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user