Nonce for CSRF protection
This commit is contained in:
parent
2fef1766f4
commit
1a752d4343
@ -68,6 +68,8 @@ import Network.Wai.Parse hiding (FileInfo)
|
|||||||
import qualified Network.Wai.Parse as NWP
|
import qualified Network.Wai.Parse as NWP
|
||||||
import Data.String (fromString)
|
import Data.String (fromString)
|
||||||
import Web.Routes
|
import Web.Routes
|
||||||
|
import Control.Arrow (first)
|
||||||
|
import System.Random (randomR, newStdGen)
|
||||||
|
|
||||||
#if TEST
|
#if TEST
|
||||||
import Test.Framework (testGroup, Test)
|
import Test.Framework (testGroup, Test)
|
||||||
@ -264,7 +266,9 @@ toWaiApp' y segments env = do
|
|||||||
let eh er = runHandler (errorHandler' er) render eurl' id y id
|
let eh er = runHandler (errorHandler' er) render eurl' id y id
|
||||||
let ya = runHandler h render eurl' id y id
|
let ya = runHandler h render eurl' id y id
|
||||||
(s, hs, ct, c, sessionFinal) <- unYesodApp ya eh rr types
|
(s, hs, ct, c, sessionFinal) <- unYesodApp ya eh rr types
|
||||||
let sessionVal = encodeSession key' exp' host sessionFinal
|
let sessionVal = encodeSession key' exp' host
|
||||||
|
$ (nonceKey, reqNonce rr)
|
||||||
|
: sessionFinal
|
||||||
let hs' = AddCookie (clientSessionDuration y) sessionName
|
let hs' = AddCookie (clientSessionDuration y) sessionName
|
||||||
(bsToChars sessionVal)
|
(bsToChars sessionVal)
|
||||||
: hs
|
: hs
|
||||||
@ -328,7 +332,27 @@ parseWaiRequest env session' = do
|
|||||||
Nothing -> langs''
|
Nothing -> langs''
|
||||||
Just x -> x : langs''
|
Just x -> x : langs''
|
||||||
rbthunk <- iothunk $ rbHelper env
|
rbthunk <- iothunk $ rbHelper env
|
||||||
return $ Request gets' cookies' session' rbthunk env langs'''
|
nonce <- case lookup nonceKey session' of
|
||||||
|
Just x -> return x
|
||||||
|
Nothing -> do
|
||||||
|
g <- newStdGen
|
||||||
|
return $ fst $ randomString 10 g
|
||||||
|
return $ Request gets' cookies' session' rbthunk env langs''' nonce
|
||||||
|
where
|
||||||
|
randomString len =
|
||||||
|
first (map toChar) . sequence' (replicate len (randomR (0, 61)))
|
||||||
|
sequence' [] g = ([], g)
|
||||||
|
sequence' (f:fs) g =
|
||||||
|
let (f', g') = f g
|
||||||
|
(fs', g'') = sequence' fs g'
|
||||||
|
in (f' : fs', g'')
|
||||||
|
toChar i
|
||||||
|
| i < 26 = toEnum $ i + fromEnum 'A'
|
||||||
|
| i < 52 = toEnum $ i + fromEnum 'a' - 26
|
||||||
|
| otherwise = toEnum $ i + fromEnum '0' - 52
|
||||||
|
|
||||||
|
nonceKey :: String
|
||||||
|
nonceKey = "_NONCE"
|
||||||
|
|
||||||
rbHelper :: W.Request -> IO RequestBodyContents
|
rbHelper :: W.Request -> IO RequestBodyContents
|
||||||
rbHelper = fmap (fix1 *** map fix2) . parseRequestBody lbsSink where
|
rbHelper = fmap (fix1 *** map fix2) . parseRequestBody lbsSink where
|
||||||
|
|||||||
@ -12,6 +12,8 @@ module Yesod.Form
|
|||||||
, FormFieldSettings (..)
|
, FormFieldSettings (..)
|
||||||
, Textarea (..)
|
, Textarea (..)
|
||||||
, FieldInfo (..)
|
, FieldInfo (..)
|
||||||
|
-- ** Utilities
|
||||||
|
, formFailures
|
||||||
-- * Type synonyms
|
-- * Type synonyms
|
||||||
, Form
|
, Form
|
||||||
, Formlet
|
, Formlet
|
||||||
@ -92,13 +94,30 @@ fieldsToDivs = mapFormXml $ mapM_ go
|
|||||||
clazz fi = if fiRequired fi then "required" else "optional"
|
clazz fi = if fiRequired fi then "required" else "optional"
|
||||||
|
|
||||||
-- | Run a form against POST parameters.
|
-- | Run a form against POST parameters.
|
||||||
runFormPost :: GForm s m xml a -> GHandler s m (FormResult a, xml, Enctype)
|
--
|
||||||
|
-- This function includes CSRF protection by checking a nonce value. You must
|
||||||
|
-- therefore embed this nonce in the form as a hidden field; that is the
|
||||||
|
-- meaning of the fourth element in the tuple.
|
||||||
|
runFormPost :: GForm s m xml a -> GHandler s m (FormResult a, xml, Enctype, Html)
|
||||||
runFormPost f = do
|
runFormPost f = do
|
||||||
rr <- getRequest
|
rr <- getRequest
|
||||||
(pp, files) <- liftIO $ reqRequestBody rr
|
(pp, files) <- liftIO $ reqRequestBody rr
|
||||||
runFormGeneric pp files f
|
nonce <- fmap reqNonce getRequest
|
||||||
|
(res, xml, enctype) <- runFormGeneric pp files f
|
||||||
|
let res' =
|
||||||
|
case res of
|
||||||
|
FormSuccess x ->
|
||||||
|
if lookup nonceName pp == Just nonce
|
||||||
|
then FormSuccess x
|
||||||
|
else FormFailure ["As a protection against cross-site request forgery attacks, please confirm your form submission."]
|
||||||
|
_ -> res
|
||||||
|
return (res', xml, enctype, hidden nonce)
|
||||||
|
where
|
||||||
|
nonceName = "_nonce"
|
||||||
|
hidden nonce = [$hamlet|%input!type=hidden!name=$nonceName$!value=$nonce$|]
|
||||||
|
|
||||||
-- | Run a form against POST parameters.
|
-- | Run a form against POST parameters. Please note that this does not provide
|
||||||
|
-- CSRF protection.
|
||||||
runFormMonadPost :: GFormMonad s m a -> GHandler s m (a, Enctype)
|
runFormMonadPost :: GFormMonad s m a -> GHandler s m (a, Enctype)
|
||||||
runFormMonadPost f = do
|
runFormMonadPost f = do
|
||||||
rr <- getRequest
|
rr <- getRequest
|
||||||
@ -106,9 +125,14 @@ runFormMonadPost f = do
|
|||||||
runFormGeneric pp files f
|
runFormGeneric pp files f
|
||||||
|
|
||||||
-- | Run a form against POST parameters, disregarding the resulting HTML and
|
-- | Run a form against POST parameters, disregarding the resulting HTML and
|
||||||
-- returning an error response on invalid input.
|
-- returning an error response on invalid input. Note: this does /not/ perform
|
||||||
|
-- CSRF protection.
|
||||||
runFormPost' :: GForm sub y xml a -> GHandler sub y a
|
runFormPost' :: GForm sub y xml a -> GHandler sub y a
|
||||||
runFormPost' = helper <=< runFormPost
|
runFormPost' f = do
|
||||||
|
rr <- getRequest
|
||||||
|
(pp, files) <- liftIO $ reqRequestBody rr
|
||||||
|
x <- runFormGeneric pp files f
|
||||||
|
helper x
|
||||||
|
|
||||||
-- | Run a form against GET parameters, disregarding the resulting HTML and
|
-- | Run a form against GET parameters, disregarding the resulting HTML and
|
||||||
-- returning an error response on invalid input.
|
-- returning an error response on invalid input.
|
||||||
@ -225,3 +249,7 @@ toLabel (x:rest) = toUpper x : go rest
|
|||||||
go (c:cs)
|
go (c:cs)
|
||||||
| isUpper c = ' ' : c : go cs
|
| isUpper c = ' ' : c : go cs
|
||||||
| otherwise = c : go cs
|
| otherwise = c : go cs
|
||||||
|
|
||||||
|
formFailures :: FormResult a -> Maybe [String]
|
||||||
|
formFailures (FormFailure x) = Just x
|
||||||
|
formFailures _ = Nothing
|
||||||
|
|||||||
@ -143,7 +143,7 @@ crudHelper
|
|||||||
-> GHandler (Crud master a) master RepHtml
|
-> GHandler (Crud master a) master RepHtml
|
||||||
crudHelper title me isPost = do
|
crudHelper title me isPost = do
|
||||||
crud <- getYesodSub
|
crud <- getYesodSub
|
||||||
(errs, form, enctype) <- runFormPost $ toForm $ fmap snd me
|
(errs, form, enctype, hidden) <- runFormPost $ toForm $ fmap snd me
|
||||||
toMaster <- getRouteToMaster
|
toMaster <- getRouteToMaster
|
||||||
case (isPost, errs) of
|
case (isPost, errs) of
|
||||||
(True, FormSuccess a) -> do
|
(True, FormSuccess a) -> do
|
||||||
@ -166,6 +166,7 @@ crudHelper title me isPost = do
|
|||||||
^form^
|
^form^
|
||||||
%tr
|
%tr
|
||||||
%td!colspan=2
|
%td!colspan=2
|
||||||
|
$hidden$
|
||||||
%input!type=submit
|
%input!type=submit
|
||||||
$maybe me e
|
$maybe me e
|
||||||
\ $
|
\ $
|
||||||
|
|||||||
@ -108,6 +108,8 @@ data Request = Request
|
|||||||
, reqWaiRequest :: W.Request
|
, reqWaiRequest :: W.Request
|
||||||
-- | Languages which the client supports.
|
-- | Languages which the client supports.
|
||||||
, reqLangs :: [String]
|
, reqLangs :: [String]
|
||||||
|
-- | A random, session-specific nonce used to prevent CSRF attacks.
|
||||||
|
, reqNonce :: String
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup' :: Eq a => a -> [(a, b)] -> [b]
|
lookup' :: Eq a => a -> [(a, b)] -> [b]
|
||||||
|
|||||||
@ -61,7 +61,7 @@ getRootR = defaultLayout $ flip wrapWidget wrapper $ do
|
|||||||
addHead [$hamlet|%meta!keywords=haskell|]
|
addHead [$hamlet|%meta!keywords=haskell|]
|
||||||
|
|
||||||
handleFormR = do
|
handleFormR = do
|
||||||
(res, form, enctype) <- runFormPost $ fieldsToTable $ (,,,,,,,,,,)
|
(res, form, enctype, hidden) <- runFormPost $ fieldsToTable $ (,,,,,,,,,,)
|
||||||
<$> stringField (FormFieldSettings "My Field" "Some tooltip info" Nothing Nothing) Nothing
|
<$> stringField (FormFieldSettings "My Field" "Some tooltip info" Nothing Nothing) Nothing
|
||||||
<*> stringField ("Another field") (Just "some default text")
|
<*> stringField ("Another field") (Just "some default text")
|
||||||
<*> intField (FormFieldSettings "A number field" "some nums" Nothing Nothing) (Just 5)
|
<*> intField (FormFieldSettings "A number field" "some nums" Nothing Nothing) (Just 5)
|
||||||
@ -103,7 +103,12 @@ textarea.html
|
|||||||
height: 150px
|
height: 150px
|
||||||
|]
|
|]
|
||||||
wrapWidget form $ \h -> [$hamlet|
|
wrapWidget form $ \h -> [$hamlet|
|
||||||
|
$maybe formFailures.res failures
|
||||||
|
%ul.errors
|
||||||
|
$forall failures f
|
||||||
|
%li $f$
|
||||||
%form!method=post!enctype=$enctype$
|
%form!method=post!enctype=$enctype$
|
||||||
|
$hidden$
|
||||||
%table
|
%table
|
||||||
^h^
|
^h^
|
||||||
%tr
|
%tr
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user