fix: високо и средно приоритетни бъгове в query, protocol, storage, vector, fts

Поправени проблеми:
- Wire protocol: bounds checking, max length limits, recursion depth limit
- SQL injection: escape-ване на quotes в exprToSql string literals
- ReDoS: escape-ване на regex metachars в LIKE/ILIKE
- Stale BTree indexes: добавен BTree.remove() + изтриване при UPDATE/DELETE
- SSL: quoteShell за всички shell команди с пътища
- Boolean literals: parser вече разпознава tkTrue/tkFalse
- Unary minus: lowerExpr map-ва ukNeg към irNeg (вместо irNot)
- Non-aggregate UDFs: lowerExpr създава irekFuncCall вместо NULL literal
- UTF-8 FTS: tokenize използва runes вместо байтове
- HNSW: добавен Lock за thread-safe insert/search

292 теста, 0 failure-а.
This commit is contained in:
2026-05-12 23:02:54 +03:00
parent 131541a0e5
commit 6aaabb518d
8 changed files with 144 additions and 29 deletions
+4 -3
View File
@@ -85,9 +85,10 @@ proc simpleStem(word: string): string =
proc tokenize*(text: string, config: TokenizerConfig = defaultTokenizerConfig()): seq[string] = proc tokenize*(text: string, config: TokenizerConfig = defaultTokenizerConfig()): seq[string] =
result = @[] result = @[]
var word = "" var word = ""
for ch in text: for r in text.runes:
if ch.isAlphaNumeric() or ch in {'_', '-'}: let rStr = $r
word.add(ch) if r.isAlpha() or rStr == "_" or rStr == "-":
word.add(rStr)
else: else:
if word.len > 0: if word.len > 0:
var token = word var token = word
+6 -6
View File
@@ -79,8 +79,8 @@ proc generateSelfSignedCert*(outputDir: string, commonName: string = "localhost"
let certPath = outputDir / (commonName & ".crt") let certPath = outputDir / (commonName & ".crt")
let keyPath = outputDir / (commonName & ".key") let keyPath = outputDir / (commonName & ".key")
createDir(outputDir) createDir(outputDir)
let cmd = "openssl req -x509 -newkey rsa:2048 -keyout " & keyPath & let cmd = "openssl req -x509 -newkey rsa:2048 -keyout " & quoteShell(keyPath) &
" -out " & certPath & " -days 365 -nodes -subj '/CN=" & commonName & "' 2>/dev/null" " -out " & quoteShell(certPath) & " -days 365 -nodes -subj " & quoteShell("/CN=" & commonName) & " 2>/dev/null"
if execShellCmd(cmd) == 0 and fileExists(certPath): if execShellCmd(cmd) == 0 and fileExists(certPath):
return (certPath, keyPath) return (certPath, keyPath)
return ("", "") return ("", "")
@@ -88,7 +88,7 @@ proc generateSelfSignedCert*(outputDir: string, commonName: string = "localhost"
proc certificateFingerprint*(certPath: string): string = proc certificateFingerprint*(certPath: string): string =
if not fileExists(certPath): if not fileExists(certPath):
return "" return ""
let cmd = "openssl x509 -in " & certPath & " -fingerprint -noout 2>/dev/null" let cmd = "openssl x509 -in " & quoteShell(certPath) & " -fingerprint -noout 2>/dev/null"
let (output, _) = execCmdEx(cmd) let (output, _) = execCmdEx(cmd)
for line in output.splitLines(): for line in output.splitLines():
if "Fingerprint=" in line: if "Fingerprint=" in line:
@@ -100,17 +100,17 @@ proc certificateFingerprint*(certPath: string): string =
proc isExpired*(certPath: string): bool = proc isExpired*(certPath: string): bool =
if not fileExists(certPath): if not fileExists(certPath):
return true return true
let cmd = "openssl x509 -in " & certPath & " -checkend 0 2>/dev/null" let cmd = "openssl x509 -in " & quoteShell(certPath) & " -checkend 0 2>/dev/null"
return execShellCmd(cmd) != 0 return execShellCmd(cmd) != 0
proc daysUntilExpiry*(certPath: string): int = proc daysUntilExpiry*(certPath: string): int =
if not fileExists(certPath): if not fileExists(certPath):
return -1 return -1
# Check if expires within 1 day # Check if expires within 1 day
let cmd1 = "openssl x509 -in " & certPath & " -checkend 86400 2>/dev/null" let cmd1 = "openssl x509 -in " & quoteShell(certPath) & " -checkend 86400 2>/dev/null"
if execShellCmd(cmd1) == 0: if execShellCmd(cmd1) == 0:
# Check if expires within 30 days # Check if expires within 30 days
let cmd30 = "openssl x509 -in " & certPath & " -checkend 2592000 2>/dev/null" let cmd30 = "openssl x509 -in " & quoteShell(certPath) & " -checkend 2592000 2>/dev/null"
if execShellCmd(cmd30) == 0: if execShellCmd(cmd30) == 0:
return 365 return 365
return 30 return 30
+36 -3
View File
@@ -4,6 +4,11 @@ import std/endians
const const
ProtocolVersion* = 1'u32 ProtocolVersion* = 1'u32
Magic* = 0x42415241'u32 # "BARA" Magic* = 0x42415241'u32 # "BARA"
MaxWireStringLen* = 64 * 1024 * 1024 # 64 MB
MaxWireArrayLen* = 1_000_000
MaxWireObjectLen* = 1_000_000
MaxWireVectorLen* = 1_000_000
MaxWireDeserializeDepth* = 100
type type
MsgKind* = enum MsgKind* = enum
@@ -110,6 +115,8 @@ proc writeBytes*(buf: var seq[byte], data: openArray[byte]) =
buf.add(b) buf.add(b)
proc readUint32*(buf: openArray[byte], pos: var int): uint32 = proc readUint32*(buf: openArray[byte], pos: var int): uint32 =
if pos + 4 > buf.len:
raise newException(ValueError, "Wire protocol: truncated uint32")
var bytes: array[4, byte] var bytes: array[4, byte]
for i in 0..3: for i in 0..3:
bytes[i] = buf[pos + i] bytes[i] = buf[pos + i]
@@ -117,6 +124,8 @@ proc readUint32*(buf: openArray[byte], pos: var int): uint32 =
pos += 4 pos += 4
proc readUint64*(buf: openArray[byte], pos: var int): uint64 = proc readUint64*(buf: openArray[byte], pos: var int): uint64 =
if pos + 8 > buf.len:
raise newException(ValueError, "Wire protocol: truncated uint64")
var bytes: array[8, byte] var bytes: array[8, byte]
for i in 0..7: for i in 0..7:
bytes[i] = buf[pos + i] bytes[i] = buf[pos + i]
@@ -125,6 +134,10 @@ proc readUint64*(buf: openArray[byte], pos: var int): uint64 =
proc readString*(buf: openArray[byte], pos: var int): string = proc readString*(buf: openArray[byte], pos: var int): string =
let len = int(readUint32(buf, pos)) let len = int(readUint32(buf, pos))
if len > MaxWireStringLen:
raise newException(ValueError, "Wire protocol: string exceeds max length")
if pos + len > buf.len:
raise newException(ValueError, "Wire protocol: truncated string data")
result = newString(len) result = newString(len)
for i in 0..<len: for i in 0..<len:
result[i] = char(buf[pos + i]) result[i] = char(buf[pos + i])
@@ -132,6 +145,10 @@ proc readString*(buf: openArray[byte], pos: var int): string =
proc readBytes*(buf: openArray[byte], pos: var int): seq[byte] = proc readBytes*(buf: openArray[byte], pos: var int): seq[byte] =
let len = int(readUint32(buf, pos)) let len = int(readUint32(buf, pos))
if len > MaxWireStringLen:
raise newException(ValueError, "Wire protocol: bytes exceed max length")
if pos + len > buf.len:
raise newException(ValueError, "Wire protocol: truncated bytes data")
result = newSeq[byte](len) result = newSeq[byte](len)
for i in 0..<len: for i in 0..<len:
result[i] = buf[pos + i] result[i] = buf[pos + i]
@@ -180,7 +197,11 @@ proc serializeValue*(buf: var seq[byte], val: WireValue) =
of fkJson: of fkJson:
buf.writeString(val.jsonVal) buf.writeString(val.jsonVal)
proc deserializeValue*(buf: openArray[byte], pos: var int): WireValue = proc deserializeValue*(buf: openArray[byte], pos: var int, depth: int = 0): WireValue =
if depth > MaxWireDeserializeDepth:
raise newException(ValueError, "Wire protocol: max deserialization depth exceeded")
if pos >= buf.len:
raise newException(ValueError, "Wire protocol: unexpected end of buffer")
let kind = FieldKind(buf[pos]) let kind = FieldKind(buf[pos])
inc pos inc pos
case kind case kind
@@ -222,20 +243,28 @@ proc deserializeValue*(buf: openArray[byte], pos: var int): WireValue =
result = WireValue(kind: fkBytes, bytesVal: readBytes(buf, pos)) result = WireValue(kind: fkBytes, bytesVal: readBytes(buf, pos))
of fkArray: of fkArray:
let count = int(readUint32(buf, pos)) let count = int(readUint32(buf, pos))
if count > MaxWireArrayLen:
raise newException(ValueError, "Wire protocol: array exceeds max length")
var arr: seq[WireValue] = @[] var arr: seq[WireValue] = @[]
for i in 0..<count: for i in 0..<count:
arr.add(deserializeValue(buf, pos)) arr.add(deserializeValue(buf, pos, depth + 1))
result = WireValue(kind: fkArray, arrayVal: arr) result = WireValue(kind: fkArray, arrayVal: arr)
of fkObject: of fkObject:
let count = int(readUint32(buf, pos)) let count = int(readUint32(buf, pos))
if count > MaxWireObjectLen:
raise newException(ValueError, "Wire protocol: object exceeds max length")
var obj: seq[(string, WireValue)] = @[] var obj: seq[(string, WireValue)] = @[]
for i in 0..<count: for i in 0..<count:
let name = readString(buf, pos) let name = readString(buf, pos)
let val = deserializeValue(buf, pos) let val = deserializeValue(buf, pos, depth + 1)
obj.add((name, val)) obj.add((name, val))
result = WireValue(kind: fkObject, objVal: obj) result = WireValue(kind: fkObject, objVal: obj)
of fkVector: of fkVector:
let count = int(readUint32(buf, pos)) let count = int(readUint32(buf, pos))
if count > MaxWireVectorLen:
raise newException(ValueError, "Wire protocol: vector exceeds max length")
if pos + count * 4 > buf.len:
raise newException(ValueError, "Wire protocol: truncated vector data")
var vec: seq[float32] = @[] var vec: seq[float32] = @[]
for i in 0..<count: for i in 0..<count:
var fl: float32 var fl: float32
@@ -283,9 +312,13 @@ proc makeQueryParamsMessage*(requestId: uint32, query: string, params: seq[WireV
proc readQueryParamsMessage*(payload: openArray[byte]): (string, seq[WireValue]) = proc readQueryParamsMessage*(payload: openArray[byte]): (string, seq[WireValue]) =
var pos = 0 var pos = 0
let queryStr = readString(payload, pos) let queryStr = readString(payload, pos)
if pos >= payload.len:
raise newException(ValueError, "Wire protocol: missing format byte")
discard payload[pos] # format byte discard payload[pos] # format byte
pos += 1 pos += 1
let paramCount = int(readUint32(payload, pos)) let paramCount = int(readUint32(payload, pos))
if paramCount > MaxWireArrayLen:
raise newException(ValueError, "Wire protocol: param count exceeds max")
var params: seq[WireValue] = @[] var params: seq[WireValue] = @[]
for i in 0..<paramCount: for i in 0..<paramCount:
params.add(deserializeValue(payload, pos)) params.add(deserializeValue(payload, pos))
+48 -5
View File
@@ -120,6 +120,9 @@ type
message*: string message*: string
keyValuePairs*: seq[(string, seq[byte])] keyValuePairs*: seq[(string, seq[byte])]
proc `==`*(a, b: IndexEntry): bool =
a.lsmKey == b.lsmKey and a.rowValue == b.rowValue
proc okResult*(rows: seq[Row] = @[], cols: seq[string] = @[], affected: int = 0, msg: string = "", proc okResult*(rows: seq[Row] = @[], cols: seq[string] = @[], affected: int = 0, msg: string = "",
kvPairs: seq[(string, seq[byte])] = @[]): ExecResult = kvPairs: seq[(string, seq[byte])] = @[]): ExecResult =
ExecResult(success: true, columns: cols, rows: rows, affectedRows: affected, message: msg, ExecResult(success: true, columns: cols, rows: rows, affectedRows: affected, message: msg,
@@ -159,7 +162,7 @@ proc exprToSql(node: Node): string =
of nkFloatLit: of nkFloatLit:
return $node.floatVal return $node.floatVal
of nkStringLit: of nkStringLit:
return "'" & node.strVal & "'" return "'" & node.strVal.replace("'", "''") & "'"
of nkBoolLit: of nkBoolLit:
return if node.boolVal: "true" else: "false" return if node.boolVal: "true" else: "false"
of nkNullLit: of nkNullLit:
@@ -521,14 +524,30 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
try: return $(pow(parseFloat(left), parseFloat(right))) try: return $(pow(parseFloat(left), parseFloat(right)))
except: return "0" except: return "0"
of irLike: of irLike:
let pattern = right.replace("%", ".*").replace("_", ".") proc escapeRe(s: string): string =
result = ""
for ch in s:
case ch
of '\\', '.', '*', '+', '?', '|', '^', '$', '(', ')', '[', ']', '{', '}':
result &= "\\" & ch
else:
result &= ch
let pattern = "^" & escapeRe(right).replace("%", ".*").replace("_", ".") & "$"
try: try:
let rePattern = re(pattern) let rePattern = re(pattern)
if left.match(rePattern): return "true" if left.match(rePattern): return "true"
except: discard except: discard
return "false" return "false"
of irILike: of irILike:
let pattern = right.toLower().replace("%", ".*").replace("_", ".") proc escapeRe(s: string): string =
result = ""
for ch in s:
case ch
of '\\', '.', '*', '+', '?', '|', '^', '$', '(', ')', '[', ']', '{', '}':
result &= "\\" & ch
else:
result &= ch
let pattern = "^" & escapeRe(right.toLower()).replace("%", ".*").replace("_", ".") & "$"
try: try:
let rePattern = re(pattern) let rePattern = re(pattern)
if left.toLower().match(rePattern): return "true" if left.toLower().match(rePattern): return "true"
@@ -776,6 +795,20 @@ proc execDelete*(ctx: ExecutionContext, table: string, key: string,
else: else:
ctx.db.delete(fullKey) ctx.db.delete(fullKey)
kvPairs.add((fullKey, @[])) kvPairs.add((fullKey, @[]))
# Update BTree indexes
for colName in ctx.btrees.keys.toSeq():
if colName.startsWith(table & "."):
let colsPart = colName[table.len + 1..^1]
let idxCols = colsPart.split(".")
var oldVals: seq[string] = @[]
for c in idxCols:
if c in oldRow:
oldVals.add(oldRow[c])
else:
oldVals.add("")
let oldIdxVal = oldVals.join("|")
if oldIdxVal.len > 0 and not isNull(oldIdxVal):
ctx.btrees[colName].remove(oldIdxVal, IndexEntry(lsmKey: fullKey, rowValue: cast[string](existingVal)))
# Update FTS indexes # Update FTS indexes
for ftsKey, ftsIdx in ctx.ftsIndexes: for ftsKey, ftsIdx in ctx.ftsIndexes:
if ftsKey.startsWith(table & "."): if ftsKey.startsWith(table & "."):
@@ -826,6 +859,9 @@ proc execUpdateRow*(ctx: ExecutionContext, table: string, key: string, sets: Tab
newVals.add(parsed[c]) newVals.add(parsed[c])
else: else:
newVals.add("") newVals.add("")
let oldIdxVal = oldVals.join("|")
if oldIdxVal.len > 0 and not isNull(oldIdxVal):
ctx.btrees[colName].remove(oldIdxVal, IndexEntry(lsmKey: fullKey, rowValue: cast[string](existing)))
let newIdxVal = newVals.join("|") let newIdxVal = newVals.join("|")
if newIdxVal.len > 0 and not isNull(newIdxVal): if newIdxVal.len > 0 and not isNull(newIdxVal):
ctx.btrees[colName].insert(newIdxVal, IndexEntry(lsmKey: fullKey, rowValue: newVal)) ctx.btrees[colName].insert(newIdxVal, IndexEntry(lsmKey: fullKey, rowValue: newVal))
@@ -1036,9 +1072,11 @@ proc lowerExpr*(node: Node): IRExpr =
result.binRight = lowerExpr(node.binRight) result.binRight = lowerExpr(node.binRight)
of nkUnaryOp: of nkUnaryOp:
result = IRExpr(kind: irekUnary) result = IRExpr(kind: irekUnary)
result.unOp = if node.unOp == ukNot: irNot else: irNot result.unOp = if node.unOp == ukNot: irNot else: irNeg
result.unExpr = lowerExpr(node.unOperand) result.unExpr = lowerExpr(node.unOperand)
of nkFuncCall: of nkFuncCall:
case node.funcName.toLower()
of "count", "sum", "avg", "min", "max":
result = IRExpr(kind: irekAggregate) result = IRExpr(kind: irekAggregate)
case node.funcName.toLower() case node.funcName.toLower()
of "count": result.aggOp = irCount of "count": result.aggOp = irCount
@@ -1046,9 +1084,14 @@ proc lowerExpr*(node: Node): IRExpr =
of "avg": result.aggOp = irAvg of "avg": result.aggOp = irAvg
of "min": result.aggOp = irMin of "min": result.aggOp = irMin
of "max": result.aggOp = irMax of "max": result.aggOp = irMax
else: result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkNull)) else: discard
result.aggArgs = @[] result.aggArgs = @[]
for arg in node.funcArgs: result.aggArgs.add(lowerExpr(arg)) for arg in node.funcArgs: result.aggArgs.add(lowerExpr(arg))
else:
result = IRExpr(kind: irekFuncCall)
result.irFunc = node.funcName
result.irFuncArgs = @[]
for arg in node.funcArgs: result.irFuncArgs.add(lowerExpr(arg))
of nkIsExpr: of nkIsExpr:
result = IRExpr(kind: irekUnary) result = IRExpr(kind: irekUnary)
result.unOp = if node.isNegated: irIsNotNull else: irIsNull result.unOp = if node.isNegated: irIsNotNull else: irIsNull
+1 -1
View File
@@ -21,7 +21,7 @@ type
IROperator* = enum IROperator* = enum
irAdd, irSub, irMul, irDiv, irMod, irPow irAdd, irSub, irMul, irDiv, irMod, irPow
irEq, irNeq, irLt, irLte, irGt, irGte irEq, irNeq, irLt, irLte, irGt, irGte
irAnd, irOr, irNot irAnd, irOr, irNot, irNeg
irIn, irNotIn irIn, irNotIn
irLike, irILike irLike, irILike
irBetween irBetween
+2 -2
View File
@@ -51,9 +51,9 @@ proc parsePrimary(p: var Parser): Node =
of tkStringLit: of tkStringLit:
discard p.advance() discard p.advance()
Node(kind: nkStringLit, strVal: tok.value, line: tok.line, col: tok.col) Node(kind: nkStringLit, strVal: tok.value, line: tok.line, col: tok.col)
of tkBoolLit: of tkBoolLit, tkTrue, tkFalse:
discard p.advance() discard p.advance()
Node(kind: nkBoolLit, boolVal: tok.value == "true", line: tok.line, col: tok.col) Node(kind: nkBoolLit, boolVal: tok.value == "true" or tok.kind == tkTrue, line: tok.line, col: tok.col)
of tkNull: of tkNull:
discard p.advance() discard p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col) Node(kind: nkNullLit, line: tok.line, col: tok.col)
+28
View File
@@ -119,3 +119,31 @@ proc scan*[K, V](btree: BTreeIndex[K, V], startKey, endKey: K): seq[(K, seq[V])]
node = node.next node = node.next
proc len*[K, V](btree: BTreeIndex[K, V]): int = btree.size proc len*[K, V](btree: BTreeIndex[K, V]): int = btree.size
proc remove*[K, V](btree: var BTreeIndex[K, V], key: K, value: V) =
proc removeRec(node: BTreeNode[K, V]): bool =
var i = 0
while i < node.keys.len and key > node.keys[i]:
inc i
if node.isLeaf:
if i < node.keys.len and key == node.keys[i]:
var vals = node.values[i]
var idx = -1
for j in 0..<vals.len:
if vals[j] == value:
idx = j
break
if idx >= 0:
vals.del(idx)
if vals.len == 0:
node.keys.del(i)
node.values.del(i)
else:
node.values[i] = vals
return true
return false
else:
return removeRec(node.children[i])
if removeRec(btree.root):
dec btree.size
+11 -1
View File
@@ -5,6 +5,7 @@ import std/random
import std/tables import std/tables
import std/sets import std/sets
import std/monotimes import std/monotimes
import std/locks
type type
DistanceMetric* = enum DistanceMetric* = enum
@@ -35,6 +36,7 @@ type
maxM*: int maxM*: int
metric*: DistanceMetric metric*: DistanceMetric
dimensions*: int dimensions*: int
lock*: Lock
IVFCluster* = object IVFCluster* = object
centroid*: Vector centroid*: Vector
@@ -92,7 +94,7 @@ proc distance*(a, b: Vector, metric: DistanceMetric): float64 =
proc newHNSWIndex*(dimensions: int, m: int = 16, efConstruction: int = 200, proc newHNSWIndex*(dimensions: int, m: int = 16, efConstruction: int = 200,
metric: DistanceMetric = dmCosine): HNSWIndex = metric: DistanceMetric = dmCosine): HNSWIndex =
HNSWIndex( var idx = HNSWIndex(
nodes: initTable[uint64, HNSWNode](), nodes: initTable[uint64, HNSWNode](),
entryPoint: 0, entryPoint: 0,
maxLevel: 0, maxLevel: 0,
@@ -102,6 +104,8 @@ proc newHNSWIndex*(dimensions: int, m: int = 16, efConstruction: int = 200,
metric: metric, metric: metric,
dimensions: dimensions, dimensions: dimensions,
) )
initLock(idx.lock)
return idx
proc randomLevel(m: int): int = proc randomLevel(m: int): int =
## Geometric distribution: probability of level L is (1/m)^L ## Geometric distribution: probability of level L is (1/m)^L
@@ -192,6 +196,8 @@ proc addBidirectionalLink(idx: HNSWIndex, nodeId, neighborId: uint64, level: int
proc insert*(idx: HNSWIndex, id: uint64, vector: Vector, proc insert*(idx: HNSWIndex, id: uint64, vector: Vector,
metadata: Table[string, string] = initTable[string, string]()) = metadata: Table[string, string] = initTable[string, string]()) =
acquire(idx.lock)
defer: release(idx.lock)
let level = randomLevel(idx.m) let level = randomLevel(idx.m)
let node = HNSWNode(id: id, vector: vector, metadata: metadata, let node = HNSWNode(id: id, vector: vector, metadata: metadata,
neighbors: newSeq[seq[uint64]](level + 1)) neighbors: newSeq[seq[uint64]](level + 1))
@@ -228,6 +234,8 @@ proc insert*(idx: HNSWIndex, id: uint64, vector: Vector,
proc search*(idx: HNSWIndex, query: Vector, k: int, proc search*(idx: HNSWIndex, query: Vector, k: int,
metric: DistanceMetric = dmCosine): seq[(uint64, float64)] = metric: DistanceMetric = dmCosine): seq[(uint64, float64)] =
acquire(idx.lock)
defer: release(idx.lock)
if idx.nodes.len == 0: if idx.nodes.len == 0:
return @[] return @[]
@@ -251,6 +259,8 @@ proc search*(idx: HNSWIndex, query: Vector, k: int,
proc searchWithFilter*(idx: HNSWIndex, query: Vector, k: int, proc searchWithFilter*(idx: HNSWIndex, query: Vector, k: int,
filter: proc(metadata: Table[string, string]): bool {.gcsafe.}, filter: proc(metadata: Table[string, string]): bool {.gcsafe.},
metric: DistanceMetric = dmCosine): seq[(uint64, float64)] = metric: DistanceMetric = dmCosine): seq[(uint64, float64)] =
acquire(idx.lock)
defer: release(idx.lock)
if idx.nodes.len == 0: if idx.nodes.len == 0:
return @[] return @[]