Skip to content

Commit 9e4d8d9

Browse files
committed
namespace: bound parser input to 253 bytes (DoS defense)
The bare-name fallback and bareDomain parser would otherwise consume arbitrarily many non-space bytes via takeWhile1 before any validation or length check. A crafted multi-megabyte token would be decoded as UTF-8 and re-parsed in full before being rejected. Introduce `boundedNonSpace` (scan with 253-byte cap) at the two takeWhile1 sites. Inputs longer than 253 bytes leave residue that parseOnly's implicit endOfInput rejects, so the parser fails fast without ever allocating the full input. The bound is the DNS full-domain limit, chosen for being a familiar ceiling generous enough to cover any realistic SimpleX name (longest plausible @user.subdomain.simplex stays well under 100 bytes). No per-label cap — SimpleX names don't go through DNS label resolution and there's no semantic reason to constrain individual labels.
1 parent 1950634 commit 9e4d8d9

2 files changed

Lines changed: 21 additions & 2 deletions

File tree

src/Simplex/Messaging/SimplexName.hs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import Control.Applicative (optional, (<|>))
2121
import qualified Data.Aeson.TH as J
2222
import qualified Data.Attoparsec.ByteString.Char8 as A
2323
import qualified Data.Attoparsec.Text as AT
24+
import Data.ByteString.Char8 (ByteString)
25+
import qualified Data.ByteString.Char8 as B
2426
import Data.Char (isDigit)
2527
import Data.Functor (($>))
2628
import Data.Text (Text)
@@ -65,19 +67,31 @@ nameLabelP = T.intercalate "-" <$> AT.takeWhile1 (\c -> isNameLetter c || isDigi
6567
-- (Cyrillic а vs ASCII a hash to different on-chain records).
6668
isNameLetter c = c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
6769

70+
-- | DoS defense for the bare-name / bare-domain entry points. The outer
71+
-- parser would otherwise `takeWhile1 (not . isSpace)` unbounded, allowing
72+
-- a crafted multi-megabyte token to be decoded and re-parsed before any
73+
-- validation. Cap at 253 bytes (DNS full-domain limit) — generous against
74+
-- any realistic SimpleX name and forces the surrounding `parseOnly`
75+
-- (which requires consuming all input) to fail on oversized inputs.
76+
boundedNonSpace :: A.Parser ByteString
77+
boundedNonSpace = do
78+
bs <- A.scan (0 :: Int) $ \i c ->
79+
if i < 253 && not (A.isSpace c) then Just (i + 1) else Nothing
80+
if B.null bs then fail "expected non-empty name token" else pure bs
81+
6882
instance StrEncoding SimplexNameInfo where
6983
strEncode SimplexNameInfo {nameType, nameDomain} =
7084
"simplex:/name" <> strEncode nameType <> strEncode nameDomain
7185
strP = optional "simplex:/name" *> ((strP >>= infoP) <|> infoP NTPublicGroup)
7286
where
7387
infoP NTPublicGroup = SimplexNameInfo NTPublicGroup <$> (strP <|> bareName)
7488
infoP NTContact = SimplexNameInfo NTContact <$> strP
75-
bareName = parseBare . safeDecodeUtf8 <$?> A.takeWhile1 (not . A.isSpace)
89+
bareName = parseBare . safeDecodeUtf8 <$?> boundedNonSpace
7690
parseBare s = (\name -> SimplexNameDomain TLDSimplex name []) <$> AT.parseOnly (nameLabelP <* AT.endOfInput) s
7791

7892
instance StrEncoding SimplexNameDomain where
7993
strEncode = encodeUtf8 . fullDomainName
80-
strP = parseDomain . safeDecodeUtf8 <$?> A.takeWhile1 (not . A.isSpace)
94+
strP = parseDomain . safeDecodeUtf8 <$?> boundedNonSpace
8195
where
8296
parseDomain s = AT.parseOnly (nameLabelP `AT.sepBy1` AT.char '.' <* AT.endOfInput) s >>= mkDomain
8397
-- All labels lowercased: DNS labels are case-insensitive, and namehash is

tests/SMPNamesTests.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,11 @@ tldWhitelistSpec = do
341341
for_ ["\1072lice.simplex", "\945pple.simplex", "\65313pple.simplex"] $ \name ->
342342
verifyRslv env RslvRequest {name, contract = addr1} `shouldBe` Nothing
343343

344+
it "rejects oversized inputs (>253 bytes) — bounded parser allocation" $ do
345+
env <- mkEnv $ TldRegistries {tldSimplex = Just addr1, tldTesting = Nothing, tldAll = Nothing}
346+
let oversize = T.replicate 254 "a" <> ".simplex"
347+
verifyRslv env RslvRequest {name = oversize, contract = addr1} `shouldBe` Nothing
348+
344349
resolverSpec :: Spec
345350
resolverSpec = do
346351
let regs = TldRegistries {tldSimplex = Just addr1, tldTesting = Nothing, tldAll = Nothing}

0 commit comments

Comments
 (0)