diff --git a/Web/Cookie.hs b/Web/Cookie.hs
--- a/Web/Cookie.hs
+++ b/Web/Cookie.hs
@@ -37,7 +37,7 @@
 import Data.Monoid (mempty, mappend, mconcat)
 import Data.Word (Word8)
 import Data.Ratio (numerator, denominator)
-import Data.Time (UTCTime, formatTime, parseTime)
+import Data.Time (UTCTime (UTCTime), toGregorian, fromGregorian, formatTime, parseTime)
 import Data.Time.Clock (DiffTime, secondsToDiffTime)
 import System.Locale (defaultTimeLocale)
 import Control.Arrow (first)
@@ -164,29 +164,23 @@
 
 parseSetCookie :: S.ByteString -> SetCookie
 parseSetCookie a = SetCookie
-    { setCookieName = key
+    { setCookieName = name
     , setCookieValue = value
-    , setCookiePath = lookup "path" pairs
+    , setCookiePath = lookup "path" flags
     , setCookieExpires =
-        lookup "expires" pairs >>= parseCookieExpires
+        lookup "expires" flags >>= parseCookieExpires
     , setCookieMaxAge =
-        lookup "max-age" pairs >>= parseCookieMaxAge
-    , setCookieDomain = lookup "domain" pairs
-    , setCookieHttpOnly = isJust $ lookup "httponly" pairs
-    , setCookieSecure = isJust $ lookup "secure" pairs
+        lookup "max-age" flags >>= parseCookieMaxAge
+    , setCookieDomain = lookup "domain" flags
+    , setCookieHttpOnly = isJust $ lookup "httponly" flags
+    , setCookieSecure = isJust $ lookup "secure" flags
     }
   where
-    (key, value, b) = parsePair a
-    pairs = map (first $ S8.map toLower) $ parsePairs b
-    parsePair bs =
-        let (k, bs') = breakDiscard 61 bs -- equals sign
-            (v, bs'') = breakDiscard 59 bs' -- semicolon
-         in (k, v, S.dropWhile (== 32) bs'') -- space
-    parsePairs bs =
-        if S.null bs
-            then []
-            else let (k, v, bs') = parsePair bs
-                  in (k, v) : parsePairs bs'
+    pairs = map (parsePair . dropSpace) $ S.split 59 a ++ [S8.empty] -- 59 = semicolon
+    (name, value) = head pairs
+    flags = map (first (S8.map toLower)) $ tail pairs
+    parsePair = breakDiscard 61 -- equals sign
+    dropSpace = S.dropWhile (== 32) -- space
 
 expiresFormat :: String
 expiresFormat = "%a, %d-%b-%Y %X GMT"
@@ -197,7 +191,17 @@
     S8.pack . formatTime defaultTimeLocale expiresFormat
 
 parseCookieExpires :: S.ByteString -> Maybe UTCTime
-parseCookieExpires = parseTime defaultTimeLocale expiresFormat . S8.unpack
+parseCookieExpires =
+    fmap fuzzYear . parseTime defaultTimeLocale expiresFormat . S8.unpack
+  where
+    -- See: https://github.com/snoyberg/cookie/issues/5
+    fuzzYear orig@(UTCTime day diff)
+        | x >= 70 && x <= 99 = addYear 1900
+        | x >= 0 && x <= 69 = addYear 2000
+        | otherwise = orig
+      where
+        (x, y, z) = toGregorian day
+        addYear x' = UTCTime (fromGregorian (x + x') y z) diff
 
 -- | Format a 'DiffTime' for a cookie.
 formatCookieMaxAge :: DiffTime -> S.ByteString
diff --git a/cookie.cabal b/cookie.cabal
--- a/cookie.cabal
+++ b/cookie.cabal
@@ -1,5 +1,5 @@
 name:            cookie
-version:         0.4.1.1
+version:         0.4.1.2
 license:         BSD3
 license-file:    LICENSE
 author:          Michael Snoyman <michael@snoyman.com>
@@ -7,7 +7,7 @@
 synopsis:        HTTP cookie parsing and rendering
 category:        Web, Yesod
 stability:       Stable
-cabal-version:   >= 1.6
+cabal-version:   >= 1.8
 build-type:      Simple
 homepage:        http://github.com/snoyberg/cookie
 
@@ -22,6 +22,22 @@
                    , deepseq
     exposed-modules: Web.Cookie
     ghc-options:     -Wall
+
+test-suite test
+    hs-source-dirs: test
+    main-is: Spec.hs
+    type: exitcode-stdio-1.0
+    build-depends: base
+                 , HUnit
+                 , QuickCheck
+                 , blaze-builder
+                 , bytestring
+                 , cookie
+                 , test-framework
+                 , test-framework-hunit
+                 , test-framework-quickcheck2
+                 , text
+                 , time
 
 source-repository head
   type:     git
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,99 @@
+import Test.Framework (defaultMain)
+import Test.Framework.Providers.QuickCheck2 (testProperty)
+import Test.Framework.Providers.HUnit (testCase)
+import Test.QuickCheck
+import Test.HUnit ((@=?), Assertion)
+
+import Web.Cookie
+import Blaze.ByteString.Builder (Builder, toLazyByteString)
+import qualified Data.ByteString as S
+import qualified Data.ByteString.Char8 as S8
+import qualified Data.ByteString.Lazy as L
+import Data.Word (Word8)
+import Control.Arrow ((***))
+import Control.Applicative ((<$>), (<*>))
+import Data.Time (UTCTime (UTCTime), toGregorian)
+import qualified Data.Text as T
+
+main :: IO ()
+main = defaultMain
+    [ testProperty "parse/render cookies" propParseRenderCookies
+    , testProperty "parse/render SetCookie" propParseRenderSetCookie
+    , testProperty "parse/render cookies text" propParseRenderCookiesText
+    , testCase "parseCookies" caseParseCookies
+    , twoDigit 24 2024
+    , twoDigit 69 2069
+    , twoDigit 70 1970
+    ]
+
+propParseRenderCookies :: Cookies' -> Bool
+propParseRenderCookies cs' =
+    parseCookies (builderToBs $ renderCookies cs) == cs
+  where
+    cs = map (fromUnChars *** fromUnChars) cs'
+
+propParseRenderCookiesText :: Cookies' -> Bool
+propParseRenderCookiesText cs' =
+    parseCookiesText (builderToBs $ renderCookiesText cs) == cs
+  where
+    cs = map (T.pack . map unChar'' *** T.pack . map unChar'') cs'
+    unChar'' = toEnum . fromEnum . unChar'
+
+fromUnChars :: [Char'] -> S.ByteString
+fromUnChars = S.pack . map unChar'
+
+builderToBs :: Builder -> S.ByteString
+builderToBs = S.concat . L.toChunks . toLazyByteString
+
+type Cookies' = [([Char'], [Char'])]
+newtype Char' = Char' { unChar' :: Word8 }
+instance Show Char' where
+    show (Char' w) = [toEnum $ fromEnum w]
+    showList = (++) . show . concatMap show
+instance Arbitrary Char' where
+    arbitrary = fmap (Char' . toEnum) $ choose (62, 125)
+
+propParseRenderSetCookie :: SetCookie -> Bool
+propParseRenderSetCookie sc =
+    parseSetCookie (builderToBs $ renderSetCookie sc) == sc
+
+instance Arbitrary SetCookie where
+    arbitrary = do
+        name <- fmap fromUnChars arbitrary
+        value <- fmap fromUnChars arbitrary
+        path <- fmap (fmap fromUnChars) arbitrary
+        expires <- fmap (parseCookieExpires . formatCookieExpires)
+                    (UTCTime <$> fmap toEnum arbitrary <*> return 0)
+        domain <- fmap (fmap fromUnChars) arbitrary
+        httponly <- arbitrary
+        secure <- arbitrary
+        return def
+            { setCookieName = name
+            , setCookieValue = value
+            , setCookiePath = path
+            , setCookieExpires = expires
+            , setCookieDomain = domain
+            , setCookieHttpOnly = httponly
+            , setCookieSecure = secure
+            }
+
+caseParseCookies :: Assertion
+caseParseCookies = do
+    let input = S8.pack "a=a1;b=b2; c=c3"
+        expected = [("a", "a1"), ("b", "b2"), ("c", "c3")]
+    map (S8.pack *** S8.pack) expected @=? parseCookies input
+
+-- Tests for two digit years, see:
+--
+-- https://github.com/snoyberg/cookie/issues/5
+twoDigit x y =
+    testCase ("year " ++ show x) (y @=? year)
+  where
+    (year, _, _) = toGregorian day
+    Just (UTCTime day _) = setCookieExpires sc
+    sc = parseSetCookie str
+    str = S8.pack $ concat
+        [ "foo=bar; Expires=Mon, 29-Jul-"
+        , show x
+        , " 04:52:08 GMT"
+        ]
