Cookie and Browser Storage Analysis โ Public-Facing Surface โ
Status: Audit โ 2026-05-20 Scope: Public-facing surface only โ Hugo site (/, /tutorials/*, /missions/*, /groups/*), App Space (hugo-apps/src/app-space), Tutorial Navigator (hugo-apps/src/navigator), Mini-Navigator, Nav Dropdown, Joule chat widget, Display app (app/display-app/), Scanner UI (UI5 + Vue variants), and the AppRouter routes that serve them. The Admin UI shell (/admin-ui/*) is explicitly excluded from this audit. Out of scope: Admin shell theme persistence, admin Fiori Elements apps, internal CAP service-to-service traffic.
This document is the cookie/storage inventory for the public-facing surface. It distinguishes HTTP cookies (sent automatically with every request to the matching origin/path) from localStorage / sessionStorage (Web Storage API, never transmitted, cleared by different rules). Both are in scope of GDPR Art. 5(3) / ePrivacy Directive (the "cookie law" applies to any storage of or access to information on the user's terminal equipment, regardless of mechanism), but the consent rules and lifetime characteristics differ.
1. Storage Mechanism Primer โ
| Mechanism | Sent on every HTTP request? | Cleared when? | Max size | Accessible from JS? | Consent regime |
|---|---|---|---|---|---|
| HTTP cookie (no flags) | Yes (matching origin/path) | At Expires / Max-Age, or on browser close if neither set ("session cookie") | ~4 KB per cookie | Yes, unless HttpOnly set | GDPR Art. 5(3): consent required unless strictly necessary |
HttpOnly cookie | Yes | Same as above | ~4 KB | No (XSS-proof) | Same as above |
Secure cookie | Yes (HTTPS only) | Same as above | ~4 KB | Same as above | Same as above |
localStorage | No (never sent) | Only by explicit JS or user action ("clear site data"). Survives browser close. | ~5โ10 MB per origin | Yes | GDPR Art. 5(3): consent required unless strictly necessary โ same legal threshold as cookies |
sessionStorage | No | When the tab is closed (not the whole browser). Per-tab, not shared between tabs. | ~5โ10 MB per origin | Yes | Same as localStorage |
| IndexedDB / Cache Storage | No | Manual or quota eviction | Large (GB) | Yes | Same |
Key implication: "We don't use cookies, only localStorage" is not a legal defense in the EU. The ePrivacy Directive, as transposed by the EU member states, applies to every form of client-side state, including localStorage, sessionStorage, IndexedDB, and Cache Storage. Use category and necessity to decide consent โ not the underlying API.
2. Inventory โ HTTP Cookies โ
The application code in this repository does not call res.cookie(), Set-Cookie, or document.cookie anywhere on the public-facing surface. All HTTP cookies observed in the request flow are set by the infrastructure layer (AppRouter, XSUAA, Cloud Foundry gorouter, the user's IdP, optional third parties when embedded).
2.1 First-party cookies set by @sap/approuter โ
Set when any route with "authenticationType": "xsuaa" is requested (e.g., /auth/user, /api/*, /admin-ui/* โ though admin-ui is out of scope, the cookie is the same one used for the public app's authenticated calls). Configured in approuter/xs-app.json and the @sap/approuter defaults.
| Cookie | Type | Flags | Lifetime | Purpose | Consent category |
|---|---|---|---|---|---|
JSESSIONID (or connect.sid depending on approuter version) | First-party | HttpOnly, Secure (in production over HTTPS), SameSite=Lax (default) | Session (cleared on browser close) โ or until logout | AppRouter session id linking the browser to the cached XSUAA OAuth tokens | Strictly necessary โ required for authenticated routes (/auth/user, /api/*, /api/qrcode, App Space progress, scanner). Exempt from prior consent under GDPR. |
XSRF-TOKEN (only on CSRF-protected destinations) | First-party | Readable to JS (so SAP UI5 can echo it back as x-csrf-token) | Session | CSRF double-submit token | Strictly necessary |
Implementation note: AppRouter session and CSRF cookies are wired through the @sap/approuter package โ verify the exact cookie names per version by inspecting a live response in DevTools. They are not configured in this repo's code.
2.2 First-party cookies set by Cloud Foundry gorouter โ
| Cookie | Type | Flags | Lifetime | Purpose | Consent category |
|---|---|---|---|---|---|
__VCAP_ID__ | First-party | Secure, HttpOnly, SameSite=Lax (gorouter defaults) | Session | Sticky-session routing โ pins a browser to a specific app instance for in-memory state continuity | Strictly necessary (operational) โ set by the platform, not the application |
2.3 Third-party cookies set by the IdP during login โ
When the user clicks login, the AppRouter redirects to XSUAA, which redirects to the configured trust:
- SAP ID Service (default trust:
accounts.sap.com) โ sets its own session cookies on theaccounts.sap.comdomain - Optional IAS (per
docs/developers/operations/ias-setup.md) โ sets cookies on the IAS tenant domain
These cookies are not set by developers.sap.com and are not in our control. They appear as third-party in the IdP popup but become first-party once the user is on the IdP origin. From a compliance standpoint they are part of the authentication flow and are strictly necessary; their cookie policy is governed by the IdP, not this app.
2.4 Third-party cookies from embedded content (deferred / on-click only) โ
The Hugo layouts include links to third-party services but do not load them eagerly โ no SDKs, pixels, or iframes are embedded on page load:
| Service | Trigger | File |
|---|---|---|
| Facebook share | User clicks "Share on Facebook" โ navigates to facebook.com/sharer/sharer.php. Sets Facebook cookies on the destination tab, not on developers.sap.com. | hugo/layouts/partials/feedback-share.html:55 |
| LinkedIn share | Same pattern โ direct link, no SDK. | hugo/layouts/partials/feedback-share.html:58 |
| Qualtrics survey | User clicks "Take our survey" โ navigates to sapinsights.eu.qualtrics.com. Qualtrics sets its own analytics cookies on its domain. | hugo/layouts/partials/feedback-share.html:43 |
| YouTube embeds | Not currently embedded by layouts. CSP allows frame-src https://www.youtube.com (approuter/xs-app.json:6) so a tutorial author could embed a YouTube video via raw HTML in markdown (Goldmark unsafe = true is enabled โ see ยง4 risk note). YouTube would then set VISITOR_INFO1_LIVE, YSC, __Secure-3PSID, etc. on the user's browser. | Author-embedded |
Compliance posture for ยง2.4: Because the third-party requests only fire on explicit user action (click), there is no third-party storage before consent. EU regulators generally accept this "click-to-load" pattern as compliant without prior consent โ but only for the static link case. Any move to embedded share widgets, tracking pixels, or auto-loaded YouTube iframes will trigger the consent requirement.
2.5 Third-party CMP cookies (TrustArc) โ
As of 2026-07-23, the site loads the TrustArc CMP (cmp = 'trustarc' in hugo/hugo.toml, property sapshared.com). TrustArc sets the following cookies on the origin on first visit:
| Cookie | Set by | Purpose | Consent category |
|---|---|---|---|
notice_gdpr_prefs, notice_behavior, cmapi_cookie_privacy, notice_preferences | TrustArc CMP (consent.trustarc.com) | Consent state per category (groups 0/1/2) โ records the visitor's cookie-consent decision | Strictly necessary โ consent record itself |
Note: These cookies are set by TrustArc's CDN script and are not controlled by application code. Their presence is required for the CMP to function.
2.6 Cookies the application does not set โ
The following common categories are absent from this codebase:
- โ No analytics cookies (no Google Analytics, no Adobe Analytics tag in the public layouts โ Adobe Analytics is server-side only via
srv/lib/adobe-analytics.js) - โ No marketing/advertising cookies
- โ No A/B testing cookies (no Optimizely, no LaunchDarkly client SDK)
- โ No CDN tracking cookies (Cloudflare
__cf_bmcould appear if CF Cloud Foundry routes through Cloudflare, but the eu10-005 SAP BTP region terminates TLS at gorouter) - โ No application-level
res.cookie()calls โ confirmed viaGrep -r "res\.cookie|setHeader.*[Cc]ookie|Set-Cookie" srv/ approuter/
3. Inventory โ Browser Storage (localStorage and sessionStorage) โ
3.1 localStorage โ persistent, survives browser close โ
| Key | Set by | Value | Purpose | Consent category |
|---|---|---|---|---|
theme | hugo/layouts/partials/head.html:26,36 | "light" | "dark" | Persists the user's theme preference across visits. Read on every page load to set <html data-theme> before paint (avoids flash of unstyled content). Falls back to prefers-color-scheme when not set. | Functional โ user-explicit preference. Defensible as strictly necessary under GDPR ("services explicitly requested by the subscriber") because the user actively toggles it. |
theme (Scanner Vue) | hugo/layouts/scanner-vue/list.html:22 | "light" | "dark" | Same as above, scoped to the scanner-vue route. Same key as the main site, so the preference is shared across the whole origin. | Functional |
Note: No keys named theme are set anywhere else on the public surface. The Admin shell uses a different key (sap-tutorials-admin-theme, app/admin-shell/webapp/Component.js) โ that's out of scope.
3.2 sessionStorage โ per-tab, cleared on tab close โ
All sessionStorage use on the public surface is in the Joule chat widget (hugo/static/js/joule.js).
| Key | Set by | Value | Purpose | Consent category |
|---|---|---|---|---|
joule.config.v1 | joule.js:217,232 | JSON { ts, value: { enabled, model, โฆ } } | 60-second TTL cache of /api/ChatConfig response โ avoids re-fetching the chat enablement config on every page navigation within the tab. | Functional / strictly necessary โ required for the Joule chat feature the user has activated by opening the panel. |
joule.history | joule.js:238,241,539 | JSON array of { role, content } messages | Preserves the chat conversation across same-tab navigation so the user doesn't lose context when they click a tutorial link from inside the chat. Cleared via the chat overflow menu's "Clear" button. | Functional โ directly enables the requested feature. |
joule.user.v1 | joule.js:350,367 | JSON { ts, value: { firstName, familyName, email, id } } with 60-second TTL | Caches the authenticated user's profile (from /auth/user) so the chat panel can render personalized greetings without re-hitting the auth endpoint on every panel open. | โ ๏ธ Functional, but contains personal data (PII). See ยง4.1 below. |
3.3 Storage the application does not use โ
- โ No
localStorageuse inhugo/static/js/app-space.js,navigator.js,nav-dropdown.js,event-display.js,scanner-vue.js - โ No
localStorage/sessionStoragein anyhugo-apps/src/Vue components - โ No storage in
app/display-app/src/ - โ No storage in
app/scanner/webapp/(UI5 scanner) - โ No IndexedDB use (verified by
Grep -r "indexedDB|openDatabase"โ no matches on public surface) - โ No Cache Storage / Service Worker registration on the public surface
4. Risks and Compliance Gaps โ
4.1 PII in sessionStorage โ joule.user.v1 โ
The Joule cache writes { firstName, familyName, email, id } to sessionStorage (joule.js:367). Email is personal data under GDPR Art. 4(1).
Why this is acceptable today:
sessionStorageis per-tab and cleared when the tab closes- The data already left the server in the
/auth/userresponse โ nothing new is exposed - Joule is an authenticated feature; the user is logged in
- 60-second TTL bounds re-use
Why a privacy review may flag it:
- A shared / kiosk computer with multiple tabs could leak the email to anyone who opens DevTools in the same tab before the user closes it
- "Clear browsing data" UX expectations: users assume "log out" wipes their email from the browser, but it remains in
sessionStorageuntil tab close (the chat's "Clear" overflow only removesjoule.history, notjoule.user.v1)
Recommendation: On logout, also clear joule.config.v1 and joule.user.v1. Consider whether storing email is needed at all โ firstName alone is enough for the greeting.
4.2 Author-injected third-party content (Hugo unsafe = true) โ
hugo/hugo.toml sets Goldmark unsafe = true, allowing raw HTML in tutorial markdown. The CSP at approuter/xs-app.json:6 allows frame-src https://www.youtube.com and broad img-src (including data:).
Risk: A tutorial author can embed:
- A YouTube
<iframe src="youtube.com/embed/...">โ sets YouTube cookies on first paint - A
<script>tag (CSP currently allows'unsafe-inline') - An
<img src="https://tracker.example/pixel.gif">โ sets third-party cookies
Compliance impact: Any of these would create cookie-setting before consent. This is the largest unbounded risk on the public surface โ it depends entirely on what authors write in markdown.
Mitigation options:
- Add an HTML sanitizer to the Hugo write path that strips
<iframe>,<script>,<object>, and external<img src>(already on TODO ยง14.2 for XSS, double-duty for cookie compliance) - Use youtube-nocookie.com embeds (no cookie until play) and update CSP
frame-srcaccordingly - Document an authoring style guide that prohibits direct iframe embeds
4.3 No consent banner exists today โ Resolved 2026-07-23 โ
Update 2026-07-23: The TrustArc CMP is now live as the default (cmp = 'trustarc'). The site serves the corporate TrustArc banner (sapshared.com property) on first visit. The in-house banner (cmp = 'inhouse') remains available as a break-glass fallback. See docs/developers/operations/consent-cmp-rollback.md.
The site sets the theme cookie/storage on first visit without prompting, and authenticated routes set session cookies on login. EU regulators generally accept these as strictly necessary (theme is user-functional; auth session is essential). However:
There is no public cookie policy page linked from the footerThere is no privacy policy pageThere is no consent UI
The cookie policy page (/cookies) and privacy policy page (/privacy) were published as part of the consent banner work in 2026-05-21. Footer links to both are included on every page.
Remaining gap: The ยง4.2 author-injected content risk (Hugo unsafe = true) is unchanged โ a tutorial author could still embed non-consented third-party content via raw HTML in markdown.
4.4 Future-proofing for analytics โ
When analytics is added (Adobe Analytics tag, Matomo, or similar):
- Server-side analytics routes data through
srv/lib/adobe-analytics.jsโ no client-side cookies. Preferred from a consent standpoint. - Client-side analytics tags (
gtag.js, Adobe Launch) always set non-essential cookies and cannot be loaded before granular consent.
5. Compliance Posture Summary โ
| Question | Answer |
|---|---|
| Does the public surface set any non-essential cookies today? | No (verified: no res.cookie in code, no analytics tags in layouts, share buttons are click-to-navigate) |
Does it use any non-essential localStorage / sessionStorage? | Probably no โ theme is functional, Joule storage is part of an explicitly-invoked feature. A privacy lawyer should confirm classification. |
| Does it have a cookie policy? | No โ gap |
| Does it have a privacy policy? | No โ gap |
| Does it have a consent banner? | Yes โ TrustArc (sapshared.com) โ live as of 2026-07-23 (cmp = 'trustarc'); in-house banner retained as fallback (cmp = 'inhouse') |
| Are there latent risks from author-injected content? | Yes โ Hugo unsafe = true allows raw iframes/scripts in tutorials |
| Does logout clear all client state? | Partially โ XSUAA logout clears auth cookies but joule.user.v1 (PII) remains in sessionStorage until tab close |
Bottom line: The public surface is probably compliant today under a "strictly necessary" reading, but lacks the transparency artifacts (cookie policy, privacy policy) that GDPR Art. 13 requires regardless of whether consent is needed. Addressing the gaps in ยง4.3 should be the first deliverable, ahead of any consent banner implementation.
6. Recommended Next Actions โ
Ordered by impact and dependency, suitable for the cookie usage and consent banner work.
- Validate this inventory with privacy/legal โ confirm the ยง2/ยง3 entries and their proposed consent classifications.
- Publish a cookie policy page at
/cookieslinked from the footer, listing the entries from ยง2 and ยง3. - Publish a privacy policy page at
/privacycovering the personal data lifecycle (account, progress tracking, audit logs). - Fix the Joule logout gap (ยง4.1) โ clear
joule.user.v1andjoule.config.v1on logout / on/auth/userreturning unauthenticated. - Sanitize tutorial HTML (ยง4.2) โ close the unbounded author-content risk before it becomes a compliance issue. This is also already on the security TODO.
- Pick a consent banner library compatible with the existing CSP โ when analytics or marketing are introduced. Self-hosted, nonce-friendly options preferred (Klaro!, CookieConsent v3, or a lightweight in-house build). Ensure "reject all" is at least as easy as "accept all" (TTDSG / EDPB guidance).
- Document a tutorial authoring style guide prohibiting direct third-party embeds, recommending
youtube-nocookie.comand avoiding pixel trackers.
7. Verification Method โ
This inventory was produced by:
Grepoverapprouter/,srv/,hugo/,hugo-apps/src/,app/display-app/src/, andapp/scanner/for the patterns:localStorage,sessionStorage,document.cookie,Set-Cookie,res.cookie,setHeader.*[Cc]ookie,cookie-parser,express-session,iframe,embed,gtag,analytics,youtube- Reading
approuter/xs-app.jsonandapprouter/server.jsfor AppRouter route configuration and CSP - Reading
hugo/layouts/partials/head.html,feedback-share.html, andhugo/layouts/scanner-vue/list.htmlfor inline storage and third-party links - Reading
hugo/static/js/joule.jsfor chat-widget storage
Excluded by design: Admin UI shell and admin Fiori Elements components โ these are authenticated tooling for staff, not public-facing, and have a separate consent posture (workplace privacy law rather than ePrivacy Directive).
A live verification (DevTools โ Application โ Cookies + Storage) on a deployed instance should be performed to confirm the AppRouter cookie names and flag values from ยง2.1 โ those depend on the @sap/approuter runtime version and cannot be derived from source code alone.