Skip to content

v1.2.1 - Safeword

Choose a tag to compare

@orneryd orneryd released this 31 Jul 14:47
· 11 commits to main since this release

[v1.2.1] - Safeword - 7/31/2026

Maintenance and security release. closes #267, #268, #269, #270

Fixed

  • Cypher WHERE ... LIMIT now preserves predicate semantics on non-streaming
    storage wrappers.

    The MATCH fast path could mark a compilable predicate as already evaluated,
    then fall back to a non-streaming node scan that never applied it. This made
    predicates such as n.content CONTAINS "safe to delete" return unrelated
    rows when a LIMIT was present. The fallback now applies the full WHERE
    predicate before limiting results, restoring Neo4j's exact-substring
    semantics for multi-word CONTAINS and preventing unsafe false-positive
    matches from reaching downstream mutations.
  • MCP discover results now use one bounded relevance scale and are ordered
    by the returned similarity value.

    Cross-chunk RRF previously controlled result order while similarity exposed
    a mixture of cosine and raw BM25 values, so an exact lexical match could carry
    a score such as 49.09 below unrelated 0.x results. Vector-backed hits now
    retain their cosine similarity through stage-2 reranking, lexical-only scores
    are monotonically normalized to [0,1], and final results are sorted by that
    same client-visible value. The min_similarity tool schema now documents and
    applies the normalized relevance contract.
  • New property-key dictionary entries are now durable before nodes and
    relationships can reference them.

    Property-key metadata was previously persisted in a best-effort transaction
    after the entity commit, allowing a cleanly acknowledged write to leave an
    unreopenable store if that metadata write failed. New dictionary tokens are
    now persisted before entity records commit, persistence errors abort the
    write, and failed allocations are staged again on retry. This prevents
    startup failures such as property key id N not in dictionary after storing
    a previously unseen property name.
  • Automatic snapshot and WAL recovery now works when the data directory is a
    container bind-mount root.

    Linux rejects attempts to rename a mount point with EBUSY, which previously
    trapped standard -v nornicdb-data:/data deployments in a restart loop after
    recovery replay had already succeeded. Auto-recovery now falls back to moving
    the corrupted store's children into a hidden forensic directory within the
    mount, then rebuilds the recovered Badger store at the original mount root.
  • OAuth browser flows now reject unsafe callback URLs and issue secure
    authentication cookies.

    The local OAuth provider validates callback URLs both before rendering
    consent and before redirecting, rejecting external hosts, non-HTTP(S)
    schemes, embedded user information, and fragments. Browser authentication
    cookies, including sessions created through Basic authentication, are now
    marked Secure, and the OAuth test harness registers callback URLs
    explicitly instead of accepting arbitrary redirect destinations.
  • Persistence operations are now confined to their configured filesystem
    roots.

    Search metadata and indexes, vector stores, build snapshots, Badger and JSON
    backups, and APOC exports now use rooted filesystem capabilities instead of
    resolving caller-influenced paths and then opening them by absolute path.
    Non-canonical and traversal paths are rejected at the operation boundary,
    database names must be a single path component before search artifacts are
    created or removed, and atomic replacement and cleanup remain inside the
    configured root.
  • Untrusted sizes and integer conversions can no longer wrap into unsafe
    allocations.

    Added checked conversion and allocation helpers across Cypher execution,
    traversal, FastRP, search and vector persistence, storage, temporal indexes,
    GraphQL, GPU backends, and protocol adapters. User-controlled LIMIT and
    dimension values are no longer used as unchecked capacity hints, serialized
    vector sizes are validated before allocation, and lossy GPU/HNSW conversions
    now fail instead of truncating or wrapping.
  • Credentials and other sensitive values are no longer exposed through
    routine logs or command output.

    Authentication, OAuth, Bolt, storage, search, replication, Heimdall, and the
    macOS menu-bar paths now omit or redact passwords, API keys, JWTs, query
    parameters, record contents, and other secret-bearing values. CLI startup
    output also avoids echoing credentials supplied through flags or connection
    strings.
  • Security-sensitive cache and composite-key hashes now use keyed SHA-256.
    Authentication cache keys and storage composite-key indexes no longer rely
    on weaker non-cryptographic hashing, reducing collision and
    hash-manipulation risk while preserving deterministic lookup within a
    running process.
  • Swagger UI bootstrap values are now rendered through safe structured
    encoding.

    Configured OpenAPI and OAuth values are no longer interpolated directly into
    executable HTML/JavaScript, preventing crafted configuration values from
    breaking out of their intended context.
  • The macOS file indexer now treats ignore rules as glob patterns without
    compiling caller-provided regular expressions.

    Direct glob matching removes regex-injection and pathological-expression
    behavior while preserving the intended ignore-file semantics; related
    menu-bar logging also no longer prints JWT or API-key details.
  • GitHub Actions workflows now use least-privilege token permissions.
    Default workflow permissions are read-only, with cache write access granted
    only to the image-build jobs that require it.
  • APOC remote URL loads are now denied by default, and internal Cypher rewrites now reuse the shared escaped-literal path.
    apoc.load.json, apoc.load.jsonArray, apoc.load.csv, and
    apoc.import.json no longer issue arbitrary outbound HTTP(S) fetches unless
    operators explicitly enable allow_remote_url_access or
    NORNICDB_APOC_SECURITY_ALLOW_REMOTE_URL_ACCESS, and explicitly allow the
    destination host. The remote fetch path now uses a hardened HTTP client with
    redirects disabled and rejects hosts that resolve to loopback, private,
    link-local, multicast, or unspecified addresses. Separately, the
    WITH/subquery/CALL rewrite paths now route string and map literal rendering
    through the shared escaped Cypher literal helpers instead of ad hoc quoting,
    closing several executable query interpolation sinks.
  • Updating a relationship a peer transaction committed after this
    transaction began is now a retryable transient conflict instead of a hard
    "not found" error.

    Inside an explicit transaction, MERGE resolves an existing relationship
    through a latest-committed lookup, but the property update read the same
    edge through the transaction's begin-time snapshot. When a peer session
    MERGEd the same relationship with different property values and committed
    after the transaction began, the edge was found but not updatable, and
    MERGE ... SET (both the plain and UNWIND batch forms) failed with
    "not found", surfaced over Bolt as the non-retryable
    Neo.ClientError.Statement.SyntaxError. Neo4j succeeds on this exact
    interleaving (its MERGE blocks on the relationship lock, re-reads, and
    applies the SET), so drivers' managed-transaction retry never engages on
    the NornicDB error. The storage layer now classifies a snapshot-invisible
    but live edge as the existing conflict shape
    (conflict: edge <id> changed after transaction start
    Neo.TransientError.Transaction.Outdated) already used for the same race
    at commit time, so managed transactions (session.ExecuteWrite) retry on
    a fresh snapshot and converge to one relationship with the retrying
    writer's values. The reclassification is tombstone-aware: an edge the peer
    actually deleted still reports "not found", and snapshot-isolated reads
    are unchanged.

What's Changed

  • fix(storage): classify post-snapshot edge updates as retryable conflicts by @linuxdynasty in #266

Full Changelog: v1.2.0...v1.2.1