# Security Guidelines for C++ Developer UI (Monitoring Pages) This document describes security requirements for C++ developers writing monitoring pages (Developer UI) in YDB. These pages are generated at runtime using `HTML(str) { ... }` macros and served by the built-in HTTP monitoring server. Example of the pull request with CSRF protection and nonce handling in HTTP responses: [#36981](https://github.com/ydb-platform/ydb/pull/36981). --- ## 1. Content Security Policy (CSP) and `nonce` > **What the PR actually enforces.** PR [#36981](https://github.com/ydb-platform/ydb/pull/36981) sets exactly one CSP directive on monitoring responses: > > ```http > Content-Security-Policy: script-src 'nonce-AbCd…==' > ``` > > There is **no** `style-src`, `font-src`, `connect-src`, `frame-src`, `img-src`, or `default-src` in the emitted header. So today only ` )___"; ``` **✅ CORRECT — generate a nonce per response, attach it to the response event, and use it in `"; } } ``` For pages served via `TEvHttpInfoRes` (local mon, not forwarded through tablets), the same `res->Nonce = nonce` assignment applies — see `Notify(...)` in [`tablet_monitoring_proxy.cpp`](../ydb/core/tablet/tablet_monitoring_proxy.cpp). Do **not** reuse a nonce across responses — generate a fresh one each time `OnRenderAppHtmlPage` is invoked. The nonce is preserved when the response is forwarded across nodes: [`TEvRemoteHttpInfoRes::SerializeToArcadiaStream`](../ydb/library/actors/core/mon.cpp) packs it alongside the HTML, so the same pattern works for remote tablet monitoring. ### Rule: NEVER weaken the `script-src` CSP Do not add `'unsafe-inline'`, `'unsafe-eval'`, or external domains to `script-src`. If a script doesn't work without `'unsafe-inline'`, rewrite it to use a nonce (see the rule above). **❌ FORBIDDEN — weakening `script-src`:** ```cpp response << "Content-Security-Policy: script-src 'unsafe-inline'\r\n"; response << "Content-Security-Policy: script-src 'self' https://cdn.example.com\r\n"; ``` ### Rule: Avoid new inline styles even though CSP does not block them today There is no `style-src` directive in the CSP header set by PR [#36981](https://github.com/ydb-platform/ydb/pull/36981), so inline styles (`style="..."` attributes and inline `"; ``` **✅ PREFER — put styles into a static CSS file served from the same origin:** ```cpp // In ydb/core/viewer/.../monitoring.css (served from /static/): // .mon-warning { color: red; margin: 5px; } // .mon-table th { text-align: center; } str << "
...
"; ``` When a stricter `style-src` is eventually added to the header, do **not** weaken it with `'unsafe-eval'` or external domains. --- ## 2. No External Resources > **Enforcement status.** Only the `script-src` row below is enforced by the CSP header from PR [#36981](https://github.com/ydb-platform/ydb/pull/36981). The other rows describe the **target policy** the codebase is moving toward — follow them in new code so that turning on the stricter header later does not break the UI. | Directive | Target policy | Enforced today? | | ------------- | ------------------------------------------- | ---------------------------------- | | `script-src` | `'self'` + nonce, no external scripts | ✅ Yes — `script-src 'nonce-…'` | | `style-src` | `'self'` only, no external stylesheets | ❌ No directive in header (see §1) | | `font-src` | `'self'`, no external fonts | ❌ No directive in header | | `connect-src` | `'self'`, no external `fetch()`/XHR | ❌ No directive in header | | `frame-src` | `'self'`, no external iframes | ❌ No directive in header | | `img-src` | `'self'` and `data:` only, no external URLs | ❌ No directive in header | ### Rule: Use only relative links in HTML generated from C++ Monitoring pages may be served under different prefixes, so generated HTML must not hardcode absolute locations. In `href`, `src`, `action`, `formaction`, `fetch()`, `$.ajax()`, etc. use only relative links. Do not use: - full URLs: `https://example.com/...`; - protocol-relative URLs: `//example.com/...`; - root-relative paths: `/get_blob`, `/static/js/...`. **❌ FORBIDDEN:** ```cpp out << "docs\n"; out << "\n"; out << "fetch('/api/data')\n"; ``` **✅ CORRECT:** ```cpp out << "docs\n"; out << "\n"; out << "fetch('api/data')\n"; ``` If a page must reference product documentation or any other external page, route it through a relative internal documentation page/redirect, or render plain text instead of a clickable external link. ### Rule: NEVER load scripts, styles, or fonts from external URLs **❌ FORBIDDEN:** ```cpp out << "\n"; out << "\n"; ``` **✅ CORRECT — use only resources served from the same origin** Bootstrap, jQuery, and tablesorter are already bundled and served by the monitoring page wrapper. Page-specific C++ renderers normally must not emit additional `\n"; ``` **✅ CORRECT (option B, `$.ajax` variant) — same idea with jQuery, if the page already uses it:** ```cpp str << "\n"; ``` For an in-repo example of this pattern see [`state_storage_state.js`](../ydb/core/cms/ui/state_storage_state.js) (`loadDistconfStatus`) — a `POST` that reads `csrf_token` from `document.cookie` and forwards it as `X-CSRF-Token`. ### Rule: GET handlers MUST NOT perform any state-changing operations GET requests are not CSRF-protected: `CheckCsrfToken` only validates the token for `POST`/`PUT`/`DELETE`/`PATCH` (see [`mon.cpp`](../ydb/core/mon/mon.cpp), `IsCsrfProtectedMethod`). If your page needs to trigger an action (restart, stop, reconfigure), use one of the protected methods — POST is the conventional choice. **❌ FORBIDDEN — side effect in GET handler:** ```cpp void RenderPage(IOutputStream& str, const TCgiParameters& params) { if (params.Has("action")) { DoSomethingDestructive(); // ← side effect triggered by GET! } // ... render HTML } ``` **✅ CORRECT — separate GET (render) from POST (action):** ```cpp // GET handler: render only void HandleGet(NMon::TEvHttpInfo::TPtr& ev) { TStringStream html; RenderPage(html, ev->Get()->Request); ReplyAndPassAway(Viewer->GetHTTPOK(Request, "text/html; charset=utf-8", html.Str())); } // POST handler: action only, no rendering void HandlePost(NMon::TEvHttpInfo::TPtr& ev) { const auto& params = ev->Get()->Request.GetParams(); if (params.Get("action") == "restart") { DoRestart(); } ReplyAndPassAway(Viewer->GetHTTPOK(Request, "text/html; charset=utf-8", "OK")); } ``` --- ## 4. No `onclick` and `onXxx` Inline Event Handlers Inline event handlers (`onclick="..."`, `onchange="..."`, etc.) are blocked by CSP `script-src` policy even with a nonce, because the nonce applies only to `\n"; ``` --- ## 5. Output Escaping Any user-controlled or externally-sourced data rendered into HTML must be escaped. **❌ FORBIDDEN — unescaped output:** ```cpp TABLED() { str << pathName; } // pathName may contain <, >, &, " TABLED() { str << errorMessage; } // error messages may contain HTML ``` **✅ CORRECT — use `HtmlEscape`:** ```cpp #include TABLED() { str << HtmlEscape(pathName); } TABLED() { str << HtmlEscape(errorMessage); } ``` For URLs in `href` attributes, use URL encoding: ```cpp str << ""; // numeric — safe str << ""; // string — must escape ``` ### Rule: NEVER interpolate dynamic values into `` substrings. A value like `O'Brien`, `foo\nbar`, or `"; ``` **✅ CORRECT — emit values as HTML-escaped `data-*` attributes and read them from JS:** ```cpp str << "
\n"; str << ""; ``` This way the script body is a fixed string with no interpolation, and every dynamic value travels through an HTML-attribute context where `HtmlEscape` is the correct tool. The same approach works for arrays/objects — serialize them in C++ to a string and put into a single `data-...` attribute, then `JSON.parse(el.dataset.items)` on the client. Inline event handlers (`onclick="..."`) and `eval`/`setTimeout('...')`-style string-eval APIs are out of scope for this rule — they are already forbidden by §4 and by `script-src` not allowing `'unsafe-eval'`. --- ## 6. Use `GetHTTPOK()` for HTTP Responses Always use [`TViewer::GetHTTPOK()`](../ydb/core/viewer/viewer.cpp) and related methods to build HTTP responses — **do not build raw HTTP response strings manually**. Always include `charset=utf-8` in the content type — `GetHTTPOK()` does not add it automatically. **✅ CORRECT:** ```cpp ReplyAndPassAway(Viewer->GetHTTPOK(Request, "text/html; charset=utf-8", htmlContent)); ``` **❌ FORBIDDEN — raw HTTP string or missing charset:** ```cpp Send(Sender, new NMon::TEvHttpInfoRes("HTTP/1.1 200 Ok\r\n\r\n" + html)); // raw string ReplyAndPassAway(Viewer->GetHTTPOK(Request, "text/html", htmlContent)); // missing charset ``` --- ## References - [OWASP CSP Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html) - [OWASP CSRF Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) - [MDN: Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)