Repository navigation
🛡️ Sentinel: Security hardening #5241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
diegosouzapw
merged 6 commits into
cheaper-inference:release/v3.8.40
from
iamedwardngo:main
Jun 28, 2026
Merged
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0e55bda
chore(docker): harden base image against container-scan CVEs (#5228)
diegosouzapw bc9d184
chore(ci): Trivy advisory scan ignores unfixed CVEs (Security-tab noi…
diegosouzapw 9b99de0
🛡️ Sentinel: Security hardening
google-labs-jules[bot] e77aee4
Merge pull request #2 from iamedwardngo/sentinel/docs-security-harden…
iamedwardngo 9a460ff
Merge branch 'pr-5241-head' into run/pr5241
diegosouzapw f2feb20
fix(security): harden docs i18n rendering (path-traversal + markdown …
iamedwardngo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| ## DOCS-XSS-TRAVERSAL - Stored XSS and Path Traversal in Documentation Rendering | ||
| **Date:** 2026-06-28 | ||
| **Vulnerability:** Stored XSS via `dangerouslySetInnerHTML` with unsanitized markdown output and potential Path Traversal in the i18n documentation fallback logic. | ||
| **Learning:** Server-side rendering of user-provided (or contributor-provided) markdown in Next.js requires explicit sanitization when using `dangerouslySetInnerHTML`. Standard library `dompurify` requires a DOM environment (provided by `jsdom`) to run on the server. Path traversal checks must combine regex validation of input segments with absolute path resolution (`path.resolve`) and a prefix check (`startsWith`) for robust defense. | ||
| **Prevention:** Always sanitize HTML rendered from markdown on the server. Use a centralized sanitizer utility. Validate all file-system related input parameters against a strict allowlist (e.g., `/^[a-z0-9-]+$/i`). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # .trivyignore — accepted-risk suppressions for the container image scan | ||
| # | ||
| # Policy (see docs/security/SUPPLY_CHAIN.md): | ||
| # - The Trivy steps in .github/workflows/docker-publish.yml run with | ||
| # `ignore-unfixed: true`, so vulnerabilities WITHOUT a published fix are | ||
| # already excluded from both the blocking CRITICAL gate and the advisory | ||
| # Security-tab upload. You do NOT need an entry here for an unfixable | ||
| # base-image OS CVE — it will not be reported. | ||
| # - This file is the single auditable home for the rare case where a *fixable* | ||
| # CVE must be temporarily accepted (e.g. the upstream fix is not yet in the | ||
| # pinned base tag, or the affected package/binary is provably unreachable | ||
| # from the proxy request surface and rebuilding now is not justified). | ||
| # | ||
| # Format — one CVE id per line, each with a justification comment and, where | ||
| # possible, an expiry, e.g.: | ||
| # # CVE-XXXX-YYYY — <why accepted>; revisit on next base-image bump (YYYY-MM-DD) | ||
| # CVE-XXXX-YYYY | ||
| # | ||
| # Keep this list SHORT and reviewed every release. Prefer fixing (rebuild on a | ||
| # patched base / bump the dep) over suppressing. Stale entries are debt. | ||
| # | ||
| # (No accepted-risk suppressions at present — ignore-unfixed covers the noise.) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import createDOMPurify from "dompurify"; | ||
| import { JSDOM } from "jsdom"; | ||
|
|
||
| let sanitizer: createDOMPurify.DOMPurifyI | null = null; | ||
|
|
||
| /** | ||
| * Get or create a server-side DOMPurify instance. | ||
| */ | ||
| function getSanitizer(): createDOMPurify.DOMPurifyI { | ||
| if (!sanitizer) { | ||
| const window = new JSDOM("").window; | ||
| sanitizer = createDOMPurify(window as unknown as Window); | ||
| } | ||
| return sanitizer; | ||
| } | ||
|
|
||
| /** | ||
| * Sanitize HTML content for documentation display. | ||
| * @param html The raw HTML to sanitize | ||
| */ | ||
| export function sanitizeDocsHtml(html: string): string { | ||
| const purify = getSanitizer(); | ||
| return purify.sanitize(html, { | ||
| USE_PROFILES: { html: true }, | ||
| ALLOWED_TAGS: [ | ||
| "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol", | ||
| "nl", "li", "ins", "del", "te", "bw", "em", "strong", "span", "hr", "br", | ||
| "div", "table", "thead", "caption", "tbody", "tr", "th", "td", "pre", | ||
| "code", "img", "details", "summary", "input" | ||
| ], | ||
| ALLOWED_ATTR: [ | ||
| "href", "name", "target", "src", "alt", "title", "class", "id", "type", | ||
| "checked", "disabled", "rel" | ||
| ], | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| // We can't easily unit test tryI18nFallback because it's internal to the page component | ||
| // and depends on process.cwd() and fs. But we can test the regex logic. | ||
|
|
||
| const LOCALE_REGEX = /^[a-z0-9-]+$/i; | ||
| const SLUG_REGEX = /^[a-z0-9-]+$/i; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two issues with this implementation:
References
|
||
|
|
||
| test("Locale and slug regex blocks common traversal patterns", () => { | ||
| const badLocales = ["..", "../..", "/etc", "es/../../etc", "es\0"]; | ||
| const badSlugs = [[".."], ["..", "foo"], ["/etc", "passwd"], ["foo", "..", "bar"]]; | ||
|
|
||
| for (const locale of badLocales) { | ||
| assert.strictEqual(LOCALE_REGEX.test(locale), false, `Should block bad locale: ${locale}`); | ||
| } | ||
|
|
||
| for (const slug of badSlugs) { | ||
| const allOk = slug.every(s => SLUG_REGEX.test(s)); | ||
| assert.strictEqual(allOk, false, `Should block bad slug: ${slug.join("/")}`); | ||
| } | ||
| }); | ||
|
|
||
| test("Locale and slug regex allows valid patterns", () => { | ||
| const goodLocales = ["en", "pt-BR", "zh-CN", "es"]; | ||
| const goodSlugs = [["getting-started"], ["architecture", "overview"], ["v1-migration"]]; | ||
|
|
||
| for (const locale of goodLocales) { | ||
| assert.strictEqual(LOCALE_REGEX.test(locale), true, `Should allow good locale: ${locale}`); | ||
| } | ||
|
|
||
| for (const slug of goodSlugs) { | ||
| const allOk = slug.every(s => SLUG_REGEX.test(s)); | ||
| assert.strictEqual(allOk, true, `Should allow good slug: ${slug.join("/")}`); | ||
| } | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { sanitizeDocsHtml } from "../../../src/lib/docsSanitizer.ts"; | ||
|
|
||
| test("sanitizeDocsHtml removes dangerous tags and attributes", () => { | ||
| const malicious = '<script>alert("xss")</script><img src="x" onerror="alert(1)"> <a href="javascript:alert(1)">link</a> <div onclick="evil()">content</div>'; | ||
| const sanitized = sanitizeDocsHtml(malicious); | ||
|
|
||
| assert.ok(!sanitized.includes("<script>"), "Should remove <script>"); | ||
| assert.ok(!sanitized.includes("onerror"), "Should remove onerror"); | ||
| assert.ok(!sanitized.includes("onclick"), "Should remove onclick"); | ||
| assert.ok(!sanitized.includes("javascript:"), "Should remove javascript: URLs"); | ||
| assert.ok(sanitized.includes("<a>link</a>") || sanitized.includes("<a >link</a>") || sanitized.includes("link"), "Should keep safe content"); | ||
| }); | ||
|
|
||
| test("sanitizeDocsHtml allows safe tags and attributes", () => { | ||
| const safe = '<h1>Title</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em>.</p><ul><li>Item</li></ul><pre><code>code</code></pre>'; | ||
| const sanitized = sanitizeDocsHtml(safe); | ||
|
|
||
| assert.ok(sanitized.includes("<h1>Title</h1>"), "Should allow <h1>"); | ||
| assert.ok(sanitized.includes("<p>"), "Should allow <p>"); | ||
| assert.ok(sanitized.includes("<strong>"), "Should allow <strong>"); | ||
| assert.ok(sanitized.includes("<ul>"), "Should allow <ul>"); | ||
| assert.ok(sanitized.includes("<li>"), "Should allow <li>"); | ||
| assert.ok(sanitized.includes("<pre>"), "Should allow <pre>"); | ||
| assert.ok(sanitized.includes("<code>"), "Should allow <code>"); | ||
| }); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allowing the
<input>tag along with attributes liketype,name,checked, anddisabledintroduces a potential security risk (Stored XSS / Phishing / UI Redressing). An attacker could inject malicious input fields (e.g., text or password inputs) to harvest credentials or mislead users. Additionally,"nl","te", and"bw"are not standard HTML tags and appear to be typos.Recommendations:
"input"fromALLOWED_TAGSand"type","checked","disabled"fromALLOWED_ATTRif they are not strictly required.uponSanitizeElement) to strictly enforce that any<input>element hastype="checkbox"anddisabledattributes, stripping any other input types."nl","te", and"bw".