Skip to content

Commit cf16997

Browse files
committed
Enforce valid values for key value keys
1 parent d952419 commit cf16997

File tree

3 files changed

+99
-0
lines changed

3 files changed

+99
-0
lines changed

nats/js/errors.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ def __str__(self) -> str:
238238
return "nats: history limited to a max of 64"
239239

240240

241+
class InvalidKeyError(Error):
242+
"""
243+
Raised when trying to put an object in Key Value with an invalid key.
244+
"""
245+
pass
246+
247+
241248
class InvalidBucketNameError(Error):
242249
"""
243250
Raised when trying to create a KV or OBJ bucket with invalid name.

nats/js/kv.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import asyncio
1818
import datetime
19+
import re
1920
from dataclasses import dataclass
2021
from typing import TYPE_CHECKING, List, Optional
2122

@@ -31,6 +32,13 @@
3132
KV_PURGE = "PURGE"
3233
MSG_ROLLUP_SUBJECT = "sub"
3334

35+
VALID_KEY_RE = re.compile(r'^[-/_=\.a-zA-Z0-9]+$')
36+
37+
def _is_key_valid(key: str) -> bool:
38+
if len(key) == 0 or key[0] == '.' or key[-1] == '.':
39+
return False
40+
return bool(VALID_KEY_RE.match(key))
41+
3442

3543
class KeyValue:
3644
"""
@@ -126,6 +134,9 @@ async def get(self, key: str, revision: Optional[int] = None) -> Entry:
126134
"""
127135
get returns the latest value for the key.
128136
"""
137+
if not _is_key_valid(key):
138+
raise nats.js.errors.InvalidKeyError
139+
129140
entry = None
130141
try:
131142
entry = await self._get(key, revision)
@@ -182,13 +193,19 @@ async def put(self, key: str, value: bytes) -> int:
182193
put will place the new value for the key into the store
183194
and return the revision number.
184195
"""
196+
if not _is_key_valid(key):
197+
raise nats.js.errors.InvalidKeyError(key)
198+
185199
pa = await self._js.publish(f"{self._pre}{key}", value)
186200
return pa.seq
187201

188202
async def create(self, key: str, value: bytes) -> int:
189203
"""
190204
create will add the key/value pair iff it does not exist.
191205
"""
206+
if not _is_key_valid(key):
207+
raise nats.js.errors.InvalidKeyError(key)
208+
192209
pa = None
193210
try:
194211
pa = await self.update(key, value, last=0)
@@ -221,6 +238,9 @@ async def update(
221238
"""
222239
update will update the value iff the latest revision matches.
223240
"""
241+
if not _is_key_valid(key):
242+
raise nats.js.errors.InvalidKeyError(key)
243+
224244
hdrs = {}
225245
if not last:
226246
last = 0
@@ -245,6 +265,9 @@ async def delete(self, key: str, last: Optional[int] = None) -> bool:
245265
"""
246266
delete will place a delete marker and remove all previous revisions.
247267
"""
268+
if not _is_key_valid(key):
269+
raise nats.js.errors.InvalidKeyError(key)
270+
248271
hdrs = {}
249272
hdrs[KV_OP] = KV_DEL
250273

tests/test_js.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2395,6 +2395,72 @@ async def error_handler(e):
23952395
with pytest.raises(BadBucketError):
23962396
await js.key_value(bucket="TEST3")
23972397

2398+
@async_test
2399+
async def test_bucket_name_validation(self):
2400+
nc = await nats.connect()
2401+
js = nc.jetstream()
2402+
2403+
invalid_bucket_names = [
2404+
" x y",
2405+
"x ",
2406+
"x!",
2407+
"xx$",
2408+
"*",
2409+
">",
2410+
"x.>",
2411+
"x.*",
2412+
".",
2413+
".x",
2414+
".x.",
2415+
"x.",
2416+
]
2417+
2418+
for bucket_name in invalid_bucket_names:
2419+
with self.subTest(bucket_name):
2420+
with pytest.raises(InvalidBucketNameError):
2421+
await js.create_key_value(
2422+
bucket=bucket_name, history=5, ttl=3600
2423+
)
2424+
2425+
with pytest.raises(InvalidBucketNameError):
2426+
await js.key_value(bucket_name)
2427+
2428+
with pytest.raises(InvalidBucketNameError):
2429+
await js.delete_key_value(bucket_name)
2430+
2431+
@async_test
2432+
async def test_key_validation(self):
2433+
nc = await nats.connect()
2434+
js = nc.jetstream()
2435+
2436+
kv = await js.create_key_value(bucket="TEST", history=5, ttl=3600)
2437+
invalid_keys = [
2438+
" x y",
2439+
"x ",
2440+
"x!",
2441+
"xx$",
2442+
"*",
2443+
">",
2444+
"x.>",
2445+
"x.*",
2446+
".",
2447+
".x",
2448+
".x.",
2449+
"x.",
2450+
]
2451+
2452+
for key in invalid_keys:
2453+
with self.subTest(key):
2454+
# Invalid put (empty)
2455+
with pytest.raises(InvalidKeyError):
2456+
await kv.put(key, b'')
2457+
2458+
with pytest.raises(InvalidKeyError):
2459+
await kv.get(key)
2460+
2461+
with pytest.raises(InvalidKeyError):
2462+
await kv.update(key, b'')
2463+
23982464
@async_test
23992465
async def test_kv_basic(self):
24002466
errors = []
@@ -2406,6 +2472,9 @@ async def error_handler(e):
24062472
nc = await nats.connect(error_cb=error_handler)
24072473
js = nc.jetstream()
24082474

2475+
with pytest.raises(nats.js.errors.InvalidBucketNameError):
2476+
await js.create_key_value(bucket="notok!")
2477+
24092478
bucket = "TEST"
24102479
kv = await js.create_key_value(
24112480
bucket=bucket,

0 commit comments

Comments
 (0)