Sonela Security review · print to PDF

Security and read-only validation

This page is for whoever has to approve letting Sonela read your business data: every guarantee below is a property of the code — a grammar, a transaction mode, a column that does not exist.

Where Sonela runs

One rung is the page your users are already on. The other two are the choice you make about your database, and they carry different trust boundaries — both are described here rather than only the flattering one.

In the page

One script tag and a widget key — the whole change to your application. The page holds nothing secret: the widget key is public by design, and the tenant identity is bound server-side from a signed token the browser cannot mint. Editing the page's JavaScript reaches no other tenant's rows, and no key of yours. An Origin header is a courtesy, never identity: sessions held to signed-in users open only on a signed voucher outsiders cannot forge — ours do, and yours can. The whole mechanism is set out under tenant identity verification below. The person on the page sees the guarantees too: a shield in the assistant's header opens them in plain words, and when page capture is on, the person being read is told.

In your network — the Sonela Gateway

One small service, on one server of yours, next to the data it reads — never on a staff machine. It dials out to us over port 443, so you open no inbound port and expose nothing to the internet. What it needs to read your data is configured on your side and stays there — run this way there is nothing for us to store, and a row holding null cannot leak. We keep a hash of the gateway key, and both validation and execution run on your side of the firewall.

On our infrastructure — a direct connection

The setup with nothing to install, and it costs you a trust decision the gateway does not. You give us what your database expects at sign-in, and our servers open the connection themselves. Those details sit on our infrastructure. We hold them encrypted with AES-256-GCM, bound to the one workspace and data source they belong to, and no API we expose reads them back — but encryption is what we do about that exposure, not a reason there isn't one, and the exposure is the thing to weigh. We refuse a target that points at our own database, and we test the connection before accepting it.

  • The three layers below all still apply — on this path they run on our infrastructure rather than yours.
  • The hop to your database is secured by what you configure on it; we do not impose that setting, so set it if your review needs it.
  • There is no fixed outbound address to put in your firewall today. We do not offer one and will not name one. If your review needs an allowlisted source, choose the gateway.

Read-only, enforced three times

Three independent layers: all of them have to fail before the assistant can change anything, and each is stated per engine below rather than borrowed across engines. Behind the gateway they run inside your network; on a direct connection they run on ours. Which machine they run on changes; that they run does not.

1

An allowlist grammar, before the query runs

Model-written SQL is refused unless every part of it is explicitly permitted — syntax nobody anticipated is denied by default.

  • Must begin with SELECT. Any semicolon is refused, so the input stays one statement.
  • Every keyword and function must be on the allowlist, per engine, and each engine's system catalogue is unreachable — pg_*, information_schema and dblink on PostgreSQL; xp_*/sp_* procedures, OPENROWSET, hints, WAITFOR and the sys* views on SQL Server, whose lexer refuses @ and [ outright; LOAD_FILE, INTO OUTFILE and the query-modifier keywords on MySQL, whose lexer refuses @, backticks and # comments outright.
  • Schema-qualified names, comma-separated FROM lists and parenthesised join groups are refused — each one evades the rewrite.
2

The engine's own guard, while it runs

Every question runs under a statement timeout of three seconds and a cap of 500 rows, so the timeout bounds what any one question can cost you. On PostgreSQL and MySQL it also runs inside a READ ONLY transaction: the database session itself refuses a write, whatever reached it. SQL Server offers no such session mode, so there the guarantee is the two layers beside this one — the only statement of the model's that can reach the server is a single validated SELECT, and every session ends in a rollback.

3

A discard, not a commit

The transaction is thrown away rather than committed: the success path rolls it back and every failure path unwinds it, and there is no commit in the query code for either to reach. A write that somehow executed would not survive the request.

Tenant isolation by rewrite, not by prompt

Every table reference is replaced with a subquery projecting only the columns you approved and filtering on the tenant column you declared. Isolation never depends on the model behaving, and a table absent from your manifest is rejected. The tenant value is a bound parameter, server-bound from a signed session: the browser cannot assert it, the model never sees it. If the database holds one organisation's data and you told us so, there is no tenant column and no such filter — you are asked which it is, and nothing is read from your schema until you answer.

What the model wrote

SELECT "Status", count(*) FROM "Orders" GROUP BY "Status"

What actually ran (PostgreSQL shown; on SQL Server and MySQL an integer flag compares with = 0)

SELECT "Status", count(*) FROM (SELECT "Id", "PlacedOn", "Status", "Total" FROM "Orders" WHERE "TenantId" = @__ai_tenant AND "IsDeleted" = false) AS "Orders" GROUP BY "Status"

Only approved columns are projected, and a table holding a column named like a secret is withheld whole. The statement is then held to the row limit.

Tenant identity verification — signed embeds

A widget key is public by design, so it can open a session but never prove who is asking. Proof is a voucher your own server signs with a secret the page never holds, and you climb to it in three states: with no secret the widget is open to the page it is on; generating the secret arms the workspace and changes nothing a visitor sees; enforcement is a separate switch you flip yourself, after which an unsigned page is refused a new session. Turning enforcement on with no secret is refused too — there would be nothing to verify against.

Two shapes your server can sign

Both are HMAC-SHA256 under your workspace's own signing secret. There is deliberately no issuer check: the secret is the trust root, and a signature that verifies is itself the workspace binding.

  • A JWT. HMAC-SHA256 is the only algorithm accepted, the audience is pinned, and an expiry is required: a token whose expiry sits more than eight hours out is refused whole, and clocks are allowed thirty seconds of skew.
  • A raw triple — user id, timestamp, signature — for a backend without a JWT library. The signature is HMAC-SHA256 over the timestamp and the user id joined by a colon; the timestamp is bare unix seconds, digits only; the signature is hex that must decode to exactly 32 bytes. Freshness is eight hours behind and thirty seconds ahead, and the comparison is fixed-time.

Verification always runs

A signature that arrives is verified whether or not the workspace demands one, and a failure is refused — a bad voucher is evidence of spoofing, never noise: it is written to your workspace's security feed, and five in 24 hours raise a probing warning in your dashboard. A valid one earns the vouched session even before you enforce; only the total absence of a signature consults your enforcement switch.

  • A partial triple — one or two of its three fields — is refused outright.
  • If both shapes arrive, both must verify.
  • A voucher sent to a workspace that has no signing secret is refused, not waved through.
  • The refusal never says which check failed.

The secret, and rotating it

sk_ plus 256 bits from a cryptographic generator, shown exactly once, at generation; at rest we hold it encrypted with AES-256-GCM, bound to its one workspace. Rotation is an immediate cutover: the moment you rotate, signatures made with the old secret stop verifying. There is no grace window today, so rotate when your servers are ready to sign with the new one.

What a voucher proves — and what it does not

It proves who your application's server says is asking. It is identity attestation, not transport secrecy: the voucher travels in the page, any script in that page can read it, and there is no replay cache — so until it expires it can be replayed by whoever captured it. The window is the expiry your signer chose, up to the eight-hour ceiling, and it is the whole blast radius, so sign short.

  • An allowed-origins list is a browser courtesy: it stops other websites, not other programs. The voucher is the one check a script cannot forge, because the secret never reaches the page.
  • Until you enforce, an unvouched session still answers — rate-limited to a per-visitor-address slice (IPv6 grouped by /64 subnet) of the workspace's own request rate, so anonymous traffic divides your capacity and never widens it. A vouched session is served at the workspace's full rate, with no per-address slice.

What the assistant sees of the page someone is on

Two separate things, and they are worth separating: a picture of a screen, and the data the page had already fetched for the person looking at it. One is sent. The other is not sent anywhere at all.

A screenshot, and only when someone asks for one

Someone stuck on a record can press the camera in the composer and send the screen with their question, so they do not have to describe what is in front of them. There is no other way to take one: nothing automatic, nothing scheduled, nothing on open.

  • It captures the whole page, not only the part on screen. Our own widget is cut out of it; nothing else is.
  • It rides one message and is detached from the composer as that message goes. It never joins the conversation history, so it is not re-sent with the next question.
  • It goes to the same AI provider as the question — yours under your key, or ours. Never logged, never written to our disks, never cached; our metering has no field an image could ride in.
  • The image is checked before it is used: an allowlist of PNG, JPEG and WebP, and a two-megabyte ceiling measured before and after decoding.

What the page's own data reader does — and where it stops

The bundle can read the data your application had already delivered to its own user's browser, and hold it in memory for the life of that page. That is the whole behaviour. It is transmitted nowhere. The request that carries a question has four fields — the message, the recent turns, and a screenshot with its type — and there is no field a captured entry could travel in. It is never written to browser storage either, so closing the tab ends it.

  • This is groundwork for a later capability, and it is honest to say it is groundwork. The assistant cannot answer from it today, and this page will not be quiet on the day that changes.
  • Before anything is held, obvious secrets are stripped: values under names like password, token, secret, api_key, card or ssn, and values shaped like a signed token or a random key. Structure is trimmed too — depth, string length and array length are all capped, and only the names of query parameters are kept, never their values.
  • That stripping is a heuristic and we call it one. It matches names and shapes, so it will not recognise personal data sitting under an ordinary business name — a notes field, a delivery address, a date of birth. It is deliberately biased towards removing too much. It is not a guarantee, and a review should not treat it as one.

Two switches over what the assistant sees, and what off actually does

Both are on when a workspace is made, because the assistant is more useful with them and you should get the useful version first. Both are yours to turn off. They are set for a whole workspace rather than for one embed, so every key in that workspace moves together — you decide the trade between context and exposure, and you decide it once per workspace.

Switch Default What turning it off does
Reading the page's own data On On a production embed the reader is never installed, so nothing is captured at all. The setting is held by us and read from us, so a page cannot quietly restore it while the assistant runs normally. Nothing was being transmitted in either state.
Page screenshots On Refused at the API, whatever the page does: a question carrying an image to a workspace with the camera off is answered with 400 and the picture reaches no model. On the page itself the composer offers no camera once the setting arrives, and the code that takes a picture is never loaded. One limit worth a reviewer's time: a load whose setting never arrived still shows the button, so a visitor there can attach a picture and be refused rather than see it greyed out.

What Sonela can see — and what it cannot

Sonela is a data processor: your content transits our service to one AI provider — yours under your key and DPA, or ours under our agreement and model choice.

Data What actually happens
The question, and the rows that answer it In transit Reach Sonela and one AI provider — yours under your DPA, or ours. Held in memory for one request, never written to our disks.
Stored questions, answers or SQL Never A metering row has counts, latency, tokens, provider and model — and no column that can hold a question, an answer or a query. It records which of your tenants asked, for billing.
Written material you upload Stored Procedures and notes you give the assistant are kept in your workspace and sent to that same provider with the question. Workspace-scoped; deleting them removes them.
Transcripts and cached answers Never Transcripts live in the visitor's browser; an answer grounded in query results is uncacheable.
A screenshot someone sends In transit Goes with that one message to the same provider. Never logged, never written to our disks, never cached, and it does not join the conversation history.
What the page's data reader holds Never sent Held in the browser's memory for the life of the page. The request that carries a question has no field it could travel in.
How your data is reached Your side The gateway you run reaches it from inside your network, with a value configured and held there. Or ours On a direct connection we hold your database sign-in details, encrypted, and reach it ourselves.
Your AI provider key Encrypted Yours: AES-256-GCM, row-bound, never returned. Never On our key: none of yours held.
Write access to your data Never Three layers refuse it, and the last of them discards the transaction rather than committing it.
Your data used to train a model Never We train nothing, and your rows reach one provider and no further.

What is written down instead: two append-only feeds in your dashboard that nothing can edit or delete. One records every refusal at your workspace's doors — a forged signature, a blocked origin, a query outside the allowlist — as a kind and a time, never the text: a feed that stored payloads would itself be the leak it exists to catch. The other records every switch an admin flips, with who flipped it and when.

On request — not shipped today

Need rows that never transit Sonela? Ask about the local data plane — the answer loop, model calls included, runs inside your network.

Ask us for the threat model.

We will send the real one: what we assume, what we deliberately do not defend against, and where the boundaries sit. We hold no security certifications today and would rather say so — what we offer instead is specificity you can check against the code.

hello@sonela.ai sonela.ai