Nonce for CSRF protection

This commit is contained in:
Michael Snoyman 2010-10-24 22:26:33 +02:00
parent 2fef1766f4
commit 1a752d4343
5 changed files with 69 additions and 9 deletions

View File

@ -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

View File

@ -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

View File

@ -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
\ $ \ $

View File

@ -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]

View File

@ -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