diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@
 (!) Breaking change
 ```
 
+2.1.0.0 [2026-09-07]
+--------------------
+```
+(!) string(utf8): Add number range guards in 'decode'
+```
+
 2.0.1.0 [2026-09-06]
 --------------------
 ```
diff --git a/mini.cabal b/mini.cabal
--- a/mini.cabal
+++ b/mini.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               mini
-version:            2.0.1.0
+version:            2.1.0.0
 license:            MIT
 license-file:       LICENSE
 author:             Victor Wallsten <victor.wallsten@protonmail.com>
diff --git a/src/Mini/String/UTF8.hs b/src/Mini/String/UTF8.hs
--- a/src/Mini/String/UTF8.hs
+++ b/src/Mini/String/UTF8.hs
@@ -7,6 +7,9 @@
 import Control.Applicative (
   (<|>),
  )
+import Control.Monad (
+  guard,
+ )
 import Data.Bits (
   complement,
   shiftL,
@@ -34,6 +37,7 @@
   toEnum,
   ($),
   (.),
+  (/=),
   (<$>),
   (==),
   (||),
@@ -73,11 +77,13 @@
   one = mask 0x80
   two = do
     w0 <- mask 0xe0
+    guard $ w0 .&. 0x1e /= 0 -- bits [8..11] /= 0
     w1 <- next
     pure $ ((w0 .&. 0x1f) `shiftL` 6) .|. w1
   three = do
     w0 <- mask 0xf0
     w1 <- next
+    guard $ w0 .&. 0x0f /= 0 || w1 .&. 0x20 /= 0 -- bits [12..16] /= 0
     w2 <- next
     let n = ((w0 .&. 0x0f) `shiftL` 12) .|. (w1 `shiftL` 6) .|. w2
     pure . bool n 0xfffd $ -- decode into replacement character U+FFFD for
@@ -86,6 +92,7 @@
   four = do
     w0 <- mask 0xf8
     w1 <- next
+    guard $ w0 .&. 0x07 /= 0 || w1 .&. 0x30 /= 0 -- bits [17..21] /= 0
     w2 <- next
     w3 <- next
     pure $
