Skip to content
Snippets Groups Projects
Verified Commit 6245f978 authored by jan.hamal.dvorak's avatar jan.hamal.dvorak
Browse files

Implement CSRF protection form field

parent 47b77f96
No related branches found
No related tags found
No related merge requests found
...@@ -4,7 +4,7 @@ cabal-version: 1.12 ...@@ -4,7 +4,7 @@ cabal-version: 1.12
-- --
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
-- --
-- hash: 00d0ac3f76ad05002c67411c92f1e77aeee6a21668bc86d354917560a1f0ae2f -- hash: 909aa9806ed1dc9c21215c17ba94cb4e184959ac38e8f35cef2091f7639d4f52
name: hikaru name: hikaru
version: 0.1.0.0 version: 0.1.0.0
...@@ -35,6 +35,7 @@ library ...@@ -35,6 +35,7 @@ library
exposed-modules: exposed-modules:
Hikaru Hikaru
Hikaru.Action Hikaru.Action
Hikaru.CSRF
Hikaru.Dispatch Hikaru.Dispatch
Hikaru.Form Hikaru.Form
Hikaru.Link Hikaru.Link
...@@ -46,7 +47,7 @@ library ...@@ -46,7 +47,7 @@ library
Paths_hikaru Paths_hikaru
hs-source-dirs: hs-source-dirs:
lib lib
default-extensions: BlockArguments DeriveGeneric FlexibleInstances GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NamedFieldPuns NoImplicitPrelude OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving DataKinds default-extensions: BlockArguments DefaultSignatures FlexibleInstances GeneralizedNewtypeDeriving GADTs LambdaCase MultiParamTypeClasses NamedFieldPuns NoImplicitPrelude OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving
ghc-options: -Wall -Wcompat ghc-options: -Wall -Wcompat
build-depends: build-depends:
aeson >=1.4 && <1.5 aeson >=1.4 && <1.5
...@@ -57,6 +58,7 @@ library ...@@ -57,6 +58,7 @@ library
, case-insensitive >=1.2 && <1.3 , case-insensitive >=1.2 && <1.3
, containers >=0.6 && <0.7 , containers >=0.6 && <0.7
, cookie >=0.4 && <0.5 , cookie >=0.4 && <0.5
, cryptonite >=0.25 && <0.26
, http-types >=0.12 && <0.13 , http-types >=0.12 && <0.13
, lucid >=2.9 && <2.10 , lucid >=2.9 && <2.10
, mtl >=2.2 && <2.3 , mtl >=2.2 && <2.3
...@@ -64,6 +66,7 @@ library ...@@ -64,6 +66,7 @@ library
, string-conversions >=0.4 && <0.5 , string-conversions >=0.4 && <0.5
, text >=1.2 && <1.3 , text >=1.2 && <1.3
, text-icu >=0.7 && <0.8 , text-icu >=0.7 && <0.8
, time >=1.8 && <1.9
, wai >=3.2 && <3.3 , wai >=3.2 && <3.3
, wai-extra >=3.0 && <3.1 , wai-extra >=3.0 && <3.1
default-language: Haskell2010 default-language: Haskell2010
...@@ -101,7 +101,7 @@ where ...@@ -101,7 +101,7 @@ where
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy as LT
import Control.Monad.Trans (lift) import Control.Monad.Trans
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
import Data.Aeson (Value, ToJSON, encode, eitherDecode') import Data.Aeson (Value, ToJSON, encode, eitherDecode')
import Data.Binary.Builder import Data.Binary.Builder
...@@ -137,11 +137,14 @@ where ...@@ -137,11 +137,14 @@ where
-- --
getActionEnv :: m ActionEnv getActionEnv :: m ActionEnv
default getActionEnv
:: (MonadTrans t, MonadAction n, m ~ t n) => m ActionEnv
getActionEnv = lift getActionEnv
-- | -- |
-- Allow access to action when building HTML responses. -- Allow access to action when building HTML responses.
-- --
instance (MonadAction m) => MonadAction (HtmlT m) where instance (MonadAction m) => MonadAction (HtmlT m)
getActionEnv = lift getActionEnv
-- | -- |
......
{-|
Module : Hikaru.CSRF
Copyright : Jan Hamal Dvořák
License : AGPL-3
Maintainer : mordae@anilinux.org
Stability : unstable
Portability : non-portable (ghc)
CSRF mitigation utilities.
-}
module Hikaru.CSRF
( MonadCsrf(..)
, generateToken
, isTokenValid
)
where
import BasePrelude
import Control.Monad.Trans
import Crypto.Hash
import Crypto.MAC.HMAC
import Data.ByteString (ByteString)
import Data.String.Conversions
import Data.Text (Text, splitOn)
import Data.Time.Clock.POSIX (getPOSIXTime)
class (MonadIO m) => MonadCsrf m where
csrfTokenValidity :: m Int64
csrfTokenSecret :: m Text
default csrfTokenValidity
:: (MonadTrans t, MonadCsrf n, m ~ t n) => m Int64
csrfTokenValidity = lift csrfTokenValidity
default csrfTokenSecret
:: (MonadTrans t, MonadCsrf n, m ~ t n) => m Text
csrfTokenSecret = lift csrfTokenSecret
-- |
-- TODO
--
generateToken :: (MonadCsrf m) => m Text
generateToken = do
now <- getTimestamp
secret <- csrfTokenSecret
let signature = sign now secret
in return $ mconcat [ cs (show now), ":", signature ]
-- |
-- TODO
--
isTokenValid :: (MonadCsrf m) => Text -> m Bool
isTokenValid token = do
case splitOn ":" token of
[time, signature] -> do
case readMaybe (cs time) of
Just (timestamp :: Int64) -> do
now <- getTimestamp
valid <- csrfTokenValidity
secret <- csrfTokenSecret
if timestamp + valid >= now
then return (sign timestamp secret == signature)
else return False
Nothing -> return False
_else -> return False
-- Internals --------------------------------------------------------------
getTimestamp :: (MonadIO m) => m Int64
getTimestamp = round <$> liftIO getPOSIXTime
sign :: Int64 -> Text -> Text
sign timestamp secret = cs $ show $ hmacGetDigest digest
where
digest = hmac timeBytes secretBytes :: HMAC SHA256
secretBytes = cs secret :: ByteString
timeBytes = cs time :: ByteString
time = show timestamp
-- vim:set ft=haskell sw=2 ts=2 et:
...@@ -11,17 +11,19 @@ This module provides applicative form handling. ...@@ -11,17 +11,19 @@ This module provides applicative form handling.
-} -}
module Hikaru.Form module Hikaru.Form
( View(..) ( MonadCsrf(..)
, View(..)
, FormNote(..) , FormNote(..)
, Form , Form
, FieldT , FieldT
, newForm , newForm
, getForm , getForm
, postForm , postForm
, inputField
, inputField'
, hiddenField , hiddenField
, hiddenField' , hiddenField'
, csrfTokenField
, inputField
, inputField'
, textArea , textArea
, textArea' , textArea'
, selectField , selectField
...@@ -47,6 +49,7 @@ where ...@@ -47,6 +49,7 @@ where
import Control.Monad.State import Control.Monad.State
import Data.Text (Text, strip) import Data.Text (Text, strip)
import Hikaru.Action import Hikaru.Action
import Hikaru.CSRF
import Hikaru.Localize import Hikaru.Localize
import Hikaru.Types import Hikaru.Types
import Lucid import Lucid
...@@ -132,7 +135,7 @@ where ...@@ -132,7 +135,7 @@ where
= FormT = FormT
{ unFormT :: ReaderT (Env l) (StateT (View l) m) a { unFormT :: ReaderT (Env l) (StateT (View l) m) a
} }
deriving (Monad, Applicative, Functor) deriving (MonadIO, Monad, Applicative, Functor)
deriving instance (Monad m) => MonadReader (Env l) (FormT l m) deriving instance (Monad m) => MonadReader (Env l) (FormT l m)
deriving instance (Monad m) => MonadState (View l) (FormT l m) deriving instance (Monad m) => MonadState (View l) (FormT l m)
...@@ -140,6 +143,8 @@ where ...@@ -140,6 +143,8 @@ where
instance MonadTrans (FormT l) where instance MonadTrans (FormT l) where
lift = FormT . lift . lift lift = FormT . lift . lift
instance (MonadCsrf m) => MonadCsrf (FormT l m)
newtype Form l m a newtype Form l m a
= Form = Form
...@@ -159,9 +164,6 @@ where ...@@ -159,9 +164,6 @@ where
r <- unFormR r <- unFormR
return $ l <*> r return $ l <*> r
instance MonadTrans (Form l) where
lift = Form . fmap Just . lift
data Env l data Env l
= Env = Env
...@@ -176,11 +178,13 @@ where ...@@ -176,11 +178,13 @@ where
= FieldT = FieldT
{ unFieldT :: ReaderT Bool (StateT (View l, Maybe a) m) b { unFieldT :: ReaderT Bool (StateT (View l, Maybe a) m) b
} }
deriving (Monad, Applicative, Functor) deriving (MonadIO, Monad, Applicative, Functor)
instance MonadTrans (FieldT l a) where instance MonadTrans (FieldT l a) where
lift = FieldT . lift . lift lift = FieldT . lift . lift
instance (MonadCsrf m) => MonadCsrf (FieldT l a m)
-- | -- |
-- Build a fresh form without using any request data. -- Build a fresh form without using any request data.
...@@ -239,6 +243,46 @@ where ...@@ -239,6 +243,46 @@ where
else return (view', value) else return (view', value)
-- |
-- TODO
--
{-
csrfTokenField :: (MonadCsrf m) => l -> Form l m Text
csrfTokenField msg = Form do
token <- generateToken
unForm $ hiddenField "_token" (Just token) do
whenChecking do
value <- fromMaybe "" <$> fieldValue
valid <- isTokenValid value
if valid
then return ()
else addNote $ NoteError msg
-}
csrfTokenField :: (MonadCsrf m) => l -> Form l m Text
csrfTokenField msg = Form do
Env{envCheck} <- ask
name' <- makeName "csrftoken"
value <- fromMaybe "" <$> formParamMaybe name'
valid <- isTokenValid value
token <- generateToken
let view = HiddenField { viewName = name'
, viewValue = Just token
, viewNotes = if envCheck && not valid
then [NoteError msg]
else []
, viewAttrs = []
}
modify (<> view)
return $ Just token
-- | -- |
-- TODO -- TODO
-- --
......
...@@ -24,8 +24,10 @@ extra-source-files: README.md ...@@ -24,8 +24,10 @@ extra-source-files: README.md
ghc-options: -Wall -Wcompat ghc-options: -Wall -Wcompat
default-extensions: default-extensions:
- BlockArguments - BlockArguments
- DefaultSignatures
- FlexibleInstances - FlexibleInstances
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving
- GADTs
- LambdaCase - LambdaCase
- MultiParamTypeClasses - MultiParamTypeClasses
- NamedFieldPuns - NamedFieldPuns
...@@ -45,6 +47,7 @@ dependencies: ...@@ -45,6 +47,7 @@ dependencies:
- case-insensitive >= 1.2 && <1.3 - case-insensitive >= 1.2 && <1.3
- containers >= 0.6 && <0.7 - containers >= 0.6 && <0.7
- cookie >= 0.4 && <0.5 - cookie >= 0.4 && <0.5
- cryptonite >= 0.25 && <0.26
- http-types >= 0.12 && <0.13 - http-types >= 0.12 && <0.13
- lucid >= 2.9 && <2.10 - lucid >= 2.9 && <2.10
- mtl >= 2.2 && <2.3 - mtl >= 2.2 && <2.3
...@@ -52,6 +55,7 @@ dependencies: ...@@ -52,6 +55,7 @@ dependencies:
- string-conversions >= 0.4 && <0.5 - string-conversions >= 0.4 && <0.5
- text >= 1.2 && <1.3 - text >= 1.2 && <1.3
- text-icu >= 0.7 && <0.8 - text-icu >= 0.7 && <0.8
- time >= 1.8 && <1.9
- wai >= 3.2 && <3.3 - wai >= 3.2 && <3.3
- wai-extra >= 3.0 && <3.1 - wai-extra >= 3.0 && <3.1
...@@ -62,6 +66,7 @@ library: ...@@ -62,6 +66,7 @@ library:
exposed-modules: exposed-modules:
- Hikaru - Hikaru
- Hikaru.Action - Hikaru.Action
- Hikaru.CSRF
- Hikaru.Dispatch - Hikaru.Dispatch
- Hikaru.Form - Hikaru.Form
- Hikaru.Link - Hikaru.Link
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment