mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-03 06:56:52 +02:00
Security hardening + gist UI fixes (#731)
* security: harden against XSS, ReDoS, path traversal, and injection Defensive fixes across the server, storage, and viewer: - XSS (CWE-79): sanitise rendered notebooks with DOMPurify, escape file names interpolated into AngularJS expressions (escapeNgString), set Mermaid securityLevel to 'strict', and stop urlRel2abs from returning javascript:/vbscript:/data:text/html URLs. - Path traversal / zip-slip (CWE-22/23/24): validate URL-derived path components before they reach the storage layer (file/webview routes + StorageBase.assertSafePath) and sanitise zip entry names on extract for both the filesystem and S3 backends. - ReDoS (CWE-1333): escape anonymization terms with catastrophic backtracking shapes to literals instead of compiling them as regexes. - Secret hardening (CWE-798): require SESSION_SECRET / OAuth creds / DB password in production, random dev SESSION_SECRET fallback. - Rate-limit spoofing (CWE-290): derive request.ip via trust-proxy hop count instead of the client-settable cf-connecting-ip header. - NoSQL injection (CWE-943): allow only plain field paths as admin sort keys. - Reject malformed streamer requests missing required string fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ui): make gists reachable/visible and clarify the ZIP button - Gist & PR routes now accept a trailing slash (/gist/:id/:path*?), so the dashboard links (which end in "/") resolve to the gist/PR page instead of falling through to the 404 route (#725). - Gist viewer picks the default tab after content loads, defaulting to "files" when files exist; previously the ng-init ran before the async load and a files-only gist rendered blank under the hidden comments tab. - Explorer toolbar: relabel ZIP to "Full repo ZIP" with a tooltip, and add tooltips to Raw/Download clarifying they apply to the current file (#721). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: report SAML-enforced orgs clearly instead of "token expired" When a repo's organization enforces SAML SSO, GitHub returns a 403 whose message differs from the OAuth-App-restriction case. That 403 fell through to the generic handler and surfaced as "token_expired", pushing users to re-login when the real fix is authorizing their token for the org. Detect the "SAML enforcement" message and raise a dedicated, actionable error instead (#379, #550). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * security: catch nested quantified groups in ReDoS guard and backslash path traversal - hasCatastrophicBacktracking now scans across nested parens ([\s\S]*?) so shapes like ((a+))+ are detected; comment reframed as a heuristic backstop rather than a proof. - file route path-traversal check now rejects backslash separators and a leading backslash, covering Windows-style "..\" payloads (CWE-22/25). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(dev): track dev-proxy script, ignore .DS_Store and .claude/ scripts/dev-proxy.js is referenced by the "dev:ui" npm script but was never committed, breaking the command on a fresh clone. Add it and ignore local-only macOS/Claude Code files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
bdfcc56d81
commit
e4ffd74068
21 files changed
+484
-23
No files matched your search
@@ -284,6 +284,29 @@ interface CompiledTermVariant {
|
||||
mask: string;
|
||||
}
|
||||
|
||||
// Detect exponential-backtracking regex shapes — a quantifier applied to a
|
||||
// group that itself contains a quantifier or top-level alternation, e.g.
|
||||
// (a+)+, (a*)*, (a|aa)+, and the nested form ((a+))+. Anonymization terms come
|
||||
// from the repository owner and are applied as live regexes against file
|
||||
// content, so a crafted term could otherwise hang the worker (ReDoS,
|
||||
// CWE-1333/624). This is a heuristic, not a proof: the lazy [\s\S]*? body
|
||||
// matches across nested parentheses so nested quantified groups are caught,
|
||||
// and it errs toward over-escaping benign regexes rather than letting a
|
||||
// dangerous one through. It is not exhaustive — exotic backtracking shapes may
|
||||
// still slip past — so it backstops, rather than replaces, any execution-time
|
||||
// bound on the regex.
|
||||
function hasCatastrophicBacktracking(src: string): boolean {
|
||||
const quantifiedGroup = /\(([\s\S]*?)\)\s*(?:[*+]|\{\d+(?:,\d*)?\})/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = quantifiedGroup.exec(src)) !== null) {
|
||||
const inner = match[1];
|
||||
if (/[*+]|\{\d+(?:,\d*)?\}/.test(inner) || inner.includes("|")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function compileTerms(terms: string[] | undefined): CompiledTermVariant[] {
|
||||
if (!terms || terms.length === 0) return [];
|
||||
const compiled: CompiledTermVariant[] = [];
|
||||
@@ -298,9 +321,16 @@ function compileTerms(terms: string[] | undefined): CompiledTermVariant[] {
|
||||
parsed.replacement !== null
|
||||
? parsed.replacement
|
||||
: config.ANONYMIZATION_MASK + "-" + (i + 1);
|
||||
// Use the term as a regex only when it both compiles AND is free of
|
||||
// catastrophic-backtracking shapes; otherwise escape it to a literal so a
|
||||
// malicious term cannot trigger ReDoS during anonymization.
|
||||
let useAsRegex = true;
|
||||
try {
|
||||
new RegExp(term, "gi");
|
||||
} catch {
|
||||
useAsRegex = false;
|
||||
}
|
||||
if (!useAsRegex || hasCatastrophicBacktracking(term)) {
|
||||
term = term.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&");
|
||||
}
|
||||
for (const variant of termVariants(term)) {
|
||||
|
||||
Reference in new issue
Block a user