Security

Verify what leaves your browser with Tabulys

Tabulys reads and compares your files in your browser. This page gives you what you need to check that no request carries their content: the Content-Security-Policy the server sends, what each of its directives allows and refuses, the requests the site sends, and two procedures to see it for yourself.

The Content-Security-Policy

The server sends this Content-Security-Policy header with every response, pages, scripts and WebAssembly modules included. The text below is read from the server configuration each time this page is served.

default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; worker-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'

default-src 'self'
Any kind of resource that no other directive names may load from this site only. It is the fallback for the directives this policy leaves out, such as media-src, manifest-src or frame-src.
script-src 'self' 'wasm-unsafe-eval'
Scripts run only from files served by this site: no inline script, no script from another origin, and no eval() or new Function(). 'wasm-unsafe-eval' lets the site compile WebAssembly, which the comparison engine (DuckDB-WASM) needs; it does not allow eval() of JavaScript.
style-src 'self'
Style sheets load only from this site; inline style elements and style attributes are refused.
img-src 'self' data:
Images load only from this site, or from data: URLs, whose content is carried inside the page or its style sheets and fetched from no server.
font-src 'self'
Fonts load only from this site.
connect-src 'self'
Scripts, workers included, can send requests (fetch, XMLHttpRequest, WebSocket, EventSource, sendBeacon) to this site only. It does not limit what is sent to this site, which is why the requests are listed below, and it does not cover a navigation to another page nor a WebRTC connection, which no directive of this policy restricts.
worker-src 'self'
Web workers and service workers start only from scripts served by this site. The comparison runs in such workers; the site registers no service worker.
object-src 'none'
No plugin content (object or embed elements) loads.
base-uri 'self'
A page cannot set its base URL to another origin, which would point its relative links and scripts elsewhere.
form-action 'self'
Forms can be submitted to this site only.
frame-ancestors 'none'
No site, this one included, can show these pages in a frame: another site cannot lay them under its own buttons (clickjacking).

The requests the site sends

All of them go to this site. A path ending with a slash covers every file under it. Beside this list, submitting the log-in or log-out form sends POST /login or POST /logout.

  • GET /: the HTML of the home page.
  • GET /compare: the HTML of the comparison page.
  • GET /compare-excel-files: the HTML of the “compare Excel files” page, the comparison page with its own title.
  • GET /compare-csv-files: the HTML of the “compare CSV files” page, the comparison page with its own title.
  • GET /excel-diff: the HTML of the “Excel diff” page, the comparison page with its own title.
  • GET /spreadsheet-compare: the HTML of the “spreadsheet compare” page, the comparison page with its own title.
  • GET /compare-two-lists-in-excel: the HTML of the “compare two lists in Excel” page, the comparison page with its own title.
  • GET /security: the HTML of this page.
  • GET /pricing: the HTML of the pricing page.
  • GET /legal-notice: the HTML of the legal notice.
  • GET /terms: the HTML of the terms of use.
  • GET /privacy: the HTML of the privacy policy.
  • GET /login: the HTML of the log-in page.
  • GET /waitlist/confirmed: the HTML of the page that confirms a place on the waiting list, opened from the confirmation email.
  • GET /build/assets/: the scripts and style sheets of the pages, the site entry on every page, the tool entry on the home page, the comparison page and the five pages “compare Excel files”, “compare CSV files”, “Excel diff”, “spreadsheet compare” and “compare two lists in Excel”, and the waitlist entry on the pricing page, and the fonts of the site. Their names carry a hash of their content.
  • GET /favicon.ico: an icon of the site.
  • GET /favicon.svg: an icon of the site.
  • GET /apple-touch-icon.png: the icon of the site on Apple devices.
  • GET /apple-touch-icon-precomposed.png: the icon older Apple devices ask for on their own.
  • GET /build/assets/: the files of the comparison engine, loaded when you drop your first file and never before: the comparison worker, DuckDB-WASM, its worker and its .wasm modules, SheetJS for Excel files and Papa Parse for CSV files. This site serves them; no third party does.
  • POST /telemetry:

    audience measurement, one JSON event per request. The server refuses any event that does not match these definitions exactly. The opt-out button at the bottom of every page stops all of them; offline, an event is lost, with no queue and no retry.

    • comparison_done: Sent once per comparison that reaches a result.
      • event_name: The name of the event. Always comparison_done.
      • browser_id: A random identifier drawn by this browser at its first comparison and kept in its local storage. Matches ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$.
      • schema_hash_a: An HMAC-SHA-256 of the normalized column names of file A, keyed by a random key that is drawn in this browser and never sent. Matches ^[0-9a-f]{64}$.
      • schema_hash_b: An HMAC-SHA-256 of the normalized column names of file B, keyed by the same key. Matches ^[0-9a-f]{64}$.
      • entry_page (optional): The path of the first page of the site this browser visited, without its query string. Matches ^/[a-z0-9/-]{0,120}$.
      • referrer_category (optional): The kind of site the first visit came from, read from the host of the referrer only. One of search, ai_assistant, other, direct.
      • browser_family: The family of the browser, read from its user agent. One of chrome, edge, firefox, safari, other.
      • row_band: The size band of the larger of the two files, in data rows. One of rows_1_to_1000, rows_1001_to_10000, rows_10001_to_100000, rows_100001_to_1000000, rows_over_1000000.
      • duration_in_milliseconds: How long the comparison took, in milliseconds. An integer from 0 to 86400000.
      • engine_version: The version number of the comparison engine. Matches ^[0-9]{1,6}$.
    • export_done: Sent once per export of a result.
      • event_name: The name of the event. Always export_done.
      • browser_id: A random identifier drawn by this browser at its first comparison and kept in its local storage. Matches ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$.
      • export_format: The format of the exported file. One of xlsx, csv.
    • save_clicked: Sent once per click on "Save for next month".
      • event_name: The name of the event. Always save_clicked.
      • browser_id: A random identifier drawn by this browser at its first comparison and kept in its local storage. Matches ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$.
      • schema_hash_a: The schema hash of file A of the result shown, as sent with comparison_done. Matches ^[0-9a-f]{64}$.
      • schema_hash_b: The schema hash of file B of the result shown, as sent with comparison_done. Matches ^[0-9a-f]{64}$.
      • entry_page (optional): The path of the first page of the site this browser visited, without its query string. Matches ^/[a-z0-9/-]{0,120}$.
      • browser_family: The family of the browser, read from its user agent. One of chrome, edge, firefox, safari, other.
    • waitlist_joined: Sent once per accepted sign-up to the waiting list, without any identifier and never joined to the address.
      • event_name: The name of the event. Always waitlist_joined.
    • import_failed: Sent when a file cannot be read or a comparison cannot complete, without any identifier.
      • event_name: The name of the event. Always import_failed.
      • file_type: The format of the file concerned, as read or as told by the extension of its name; unrecognized when neither tells it. One of xlsx, csv, unrecognized.
      • failure_cause: The cause of the failure, from a closed list. One of unrecognized_format, unsupported_spreadsheet_format, legacy_xls_unsupported, utf16_unsupported, password_protected, corrupted_file, empty_file, invalid_utf8, malformed_csv, empty_header_row, capability_missing, worker_error, engine_unavailable, memory_suspected, partition_invariant_broken, export_cell_too_long, interface_error.
  • POST /waitlist:

    a sign-up to the waiting list, sent only when you submit its form. Its JSON body holds these fields and no other:

    • consent: Always true: the box you ticked to be emailed when the monthly replay opens.
    • email: The address you typed. The server relays it to Brevo, the email service that sends the confirmation message, and does not store it.
    • website: Always empty: a field hidden from people, which only bots fill in.

Check the requests in the developer tools

  1. Open the comparison page, open the developer tools of your browser (F12, or Cmd+Option+I on macOS) and select the Network tab. Reload the page so the list starts with it.
  2. Drop two files, answer the questions until the result shows, then export it.
  3. Read every request in the list: each goes to this site, and its method and path are in the list above. Requests sent by the workers of the comparison show in the same list.
  4. Open each POST /telemetry request and read its payload: only the fields defined above, with no cell value, no column name and no file name.
  5. Select the request of the page itself and read its response headers: the Content-Security-Policy header is the policy published above, character for character.

Compare with the network cut

  1. Open the comparison page, run a first comparison to its result and export it. This loads the files of the comparison engine.
  2. Cut the network: turn off Wi-Fi or unplug the cable, or choose Offline in the Network tab of the developer tools.
  3. Without reloading the page, drop a new pair of files.
  4. The comparison completes and the export downloads.
This works only because your browser keeps the files of the engine in its HTTP cache. The site registers no service worker and preloads nothing, so reloading the page, or clearing the cache, needs the network again. A private browsing window may not keep these files in its cache: run this check in a normal window. The audience measurement events of that time are lost.