Security¶
Reports are built by the people who use them, and views travel: they are saved on servers, shared as templates and passed in URLs. pivotkit is built for that. To report a vulnerability, use the private channel described in the repository's security policy.
Views are untrusted input¶
- A view holds names only. Field names are resolved against the data's columns; aggregators,
formats, filter operators, "show as" calculations and chart types against fixed lists; colours
against
#rgband#rrggbb. A name is never evaluated as code and never used as an object key without an own-property check. - A view from a link, a pasted text, a file or the tab's storage is the same input: it goes through
normalize()against the data, and what was left out is said in plain words. A link is capped at 32,000 characters and its view at 1,000,000 characters, enforced while it is inflated (1 KiB of input at a time), so a small crafted link can neither make a large allocation nor keep the page busy (Sharing). - The tips' list in
localStorage(dismissed tips, the help switches, the menu sections' folding) is read as JSON and kept only where an entry is one of the known ids; anything else, or text that is no JSON, reads as an empty list. - Templates are views too. Every name in the Views menu, the host's
templatesincluded, is plain text (control, bidi and zero-width characters removed, at most 120 characters). A list read withparseTemplates(from a server, the browser store or an OpenAPI document'sx-pivotkit-views) has its text (5,000,000 characters) and its entries (500) capped before it is read, own properties only; the browser store's text is not parsed at all above 500,000 characters. Every view is normalized when it is loaded, and a published view leaves the report normalized.templatesFromSchemafollows local references only and fetches nothing. The report only shows what the host allows (canPublish,deletable): the host's server must check who may save, publish or delete, and validate every view it receives (Templates and shared views). chartOptionsis the host's, but often JSON from a server: only plain JSON values pass, prototype keys and functions are dropped, depth and size are capped, and the ECharts options that are markup or addresses (a stringformatter,rich,link,imageand the tooltip's element) are dropped too, so nothing in it is markup, an address or run (Charts).- A bad view does not throw: what cannot be used is dropped and reported (see Views and saved views).
- A view cannot ask for unbounded work. Its size and its parts are capped (8 fields per axis, 16
values), the work of a pivot is computed before it starts, and the groups, aggregated cells and
every dense structure (rows x columns of a table, data points of a chart) are counted against
limits before or while they are allocated; over a limit,
PivotkitErrorwithLIMIT_EXCEEDEDis thrown and the UI shows a message that says which limit and what to change.
Data is text, never markup¶
Field names, values and labels are rendered as text by React (or textContent in your own renderer).
A value like <img src=x onerror=alert(1)> shows as those characters; a browser test checks it with a
data set of labels that look like HTML.
Field labels and descriptions (fieldDefaults) are plain text too, capped in length; a "Learn more"
link is shown only for an absolute https: URL and opens with rel="noopener noreferrer".
fieldDefaultsFromSchema treats a JSON Schema or OpenAPI document as data: it follows references
inside the document only, fetches nothing, runs nothing, and caps the fields, texts, enum entries,
reference depth and the number of schema objects it reads.
Exports¶
- CSV: text that a spreadsheet would run as a formula gets a leading
': text starting with a tab or a carriage return, text whose first visible character (after spaces, line breaks and control characters) is=or@, and text starting with+or-unless it is a number as a locale writes it (-5%,-1.234,5,-1’234.5,-5,3 %,-€1.50) or a number range such as the bin label-10 – 0. - Copies (Copy table, the drill-down's Copy): the same guard on every cell, since a spreadsheet runs a pasted formula too. Copy as JSON writes values, never markup or code.
- XLSX: text is written as inline strings, never as formulas. The XML is written by pivotkit-core itself; no spreadsheet library is involved. Excel's sheet limits are checked before writing.
- Downloads use a blob URL on an
<a download>; nothing is sent anywhere. File names are made safe: path separators and characters a file system refuses are replaced. WithonExport(or the cancelablepivot-exportevent) your page gets every file and every copy first and can keep it instead.
Content-Security-Policy¶
The web component was measured in the browser tests (tests/browser/element.spec.js) in Chromium,
Firefox and WebKit of the Playwright version pinned in package.json, with this policy in a <meta> element:
default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'
- The report renders and is styled: its constructed stylesheet is not blocked by
style-src 'self', and the inline styles React sets through the CSSOM are not blocked either. - The chart works: ECharts loads from its chunk and draws on a canvas; nothing needs
'unsafe-eval'. Its tooltip shows on hover without a violation: pivotkit-react builds it as DOM nodes with styles set through the CSSOM and the data's texts astextContent, instead of ECharts' default HTML string with inlinestyleattributes (whichstyle-src-attrwould refuse). - The XLSX and PNG exports download (a blob URL on an
<a download>; nofetchofdata:); CSV takes the same path as XLSX. - No violation is reported (
securitypolicyviolation) and nothing is logged; the test checks that the policy is in force by adding an inline<style>, which is refused. - The
<style>fallback is tested on its own page (constructable stylesheets removed), without CSP.
Not covered: browsers without constructable stylesheets fall back to a <style> element, which needs
style-src 'unsafe-inline' (the element has no nonce option); only the <meta> form of the policy was
measured, not an HTTP header; script-src must allow the origin that serves dist/ and its chunks;
a page script that sets the element's properties must be a file or carry a nonce.
pivotkit-react has no measured policy of its own: what it needs depends on how your app is built and served.
Loading from another origin¶
Module scripts from another origin (a CDN) are fetched with CORS: the server must send
Access-Control-Allow-Origin for index.js and every chunk. Subresource Integrity covers only the
<script integrity> of the entry; the chunks it imports later cannot carry a hash, so pin a version in
the URL, or host dist/ yourself.
This documentation site¶
The theme's script in the header of this site asks api.github.com for the repository's stars, forks and latest release, from the reader's browser. pivotkit itself, in your app, sends nothing to a third party: it loads only its own chunks from where you host it.
Dependencies¶
- pivotkit-core has one runtime dependency: fflate for the XLSX zip.
- pivotkit-react depends on pivotkit-core and @dnd-kit; React, ReactDOM and ECharts come from your app, so their security updates stay with you.
- pivotkit-element bundles everything it needs and has no runtime dependency; the licenses of the
bundled packages are in its
dist/THIRD_PARTY_LICENSES.txt. - Every third-party package is pinned to an exact version in the repository, and CI installs without install scripts.