RedoSan Authenticity implements defense-in-depth across multiple layers: Service Worker protection, multi-stage file validation (browser + CLI), domain origin self-check, XSS prevention, prototype pollution filtering, bot/automation detection, VPN detection, on-device biometric handling, and secure coding practices. This document describes each layer in detail.
The Service Worker (sw.js) acts as a reverse proxy for all network requests, enforcing security policies across multiple whitelists. Blocked requests receive a 403 Forbidden response with a threat-detection themed HTML page. 404.html carries a mirror of these lists for offline/error pages.
The SW intercepts all fetch events and blocks requests whose URL path ends with any dangerous extension:
| Category | Count | Extensions |
|---|---|---|
| Windows executables / libraries | 9 | .exe .msi .com .scr .pif .dll .ocx .sys .drv |
| Windows scripts / installers | 21 | .bat .cmd .ps1 .psm1 .psd1 .vbs .vbe .jse .wsf .wsh .hta .ws .vb .vba .msu .msp .reg .inf .gadget .cpl .mst |
| Shell / programming languages | 8 | .sh .bash .pl .py .rb .lua .tcl .awk |
| JVM / legacy bytecode | 2 | .jar .swf |
| Linux / Unix packages | 9 | .elf .so .ko .deb .rpm .run .appimage .flatpak .snap |
| macOS application bundle | 1 | .app |
| Macro-capable documents | 8 | .xls .xlsx .ppt .pptx .rtf .odt .ods .odp |
| Archives | 1 | .zip |
| Other dangerous | 2 | .action .apk |
Total: 61 extensions.
Only whitelisted JS files may load; any other .js request is blocked with 403:
| Directory | Files |
|---|---|
| Root | sw.js, jsdom-test-config.js, dev-server.js, sw-precache.js |
Style/ |
shared.js, shared_validation.js, navigation.js, i18n.js, search.js, loader.js, mpa-router.js, music-player.js, simplified.js, simplified_countries.js, simplified_helpers.js, simplified_renderers.js |
Style/lang/ |
i18n-data.js + 8 language files (ar, de, en, es, fr, ja, ko, zh) |
Watermark/ |
watermark.js, watermark_core.js, utils.js |
Pixel_Injection/ |
pixel_injection.js, watermark_core_advanced.js, watermark_core_algorithms.js, watermark_core_transforms.js, watermark_advanced_ui.js, advanced_watermarking.js |
Audio_Watermark/ |
audio_watermark.js, audio_watermark_core.js |
Document_Watermark/ |
document_watermark.js, document_watermark_core.js, document_watermark_report.js, document_watermark_pdf.js, text_extractor.js |
Fingerprint/ |
hashing.js, hashing_perceptual.js, fingerprint_ui.js, hash_worker.js |
C2PA/ |
c2pa.js, cbor.js |
Certificate/ |
certificate.js, certificate_pdf.js, certificate_docx.js, certificate_epub.js, certificate_ots.js, certificate_utils.js |
Timestamp/, Metadata/, ID_Forge/ |
timestamp.js, metadata.js, id_forge.js |
Forensic/ |
forensic.js, forensic_core.js (each listed twice in the array) |
Face_Biometric/ |
15 files (engine, registry, ui, crypto, camera, liveness, antispoof, embed_onnx, webauthn, vc, biohash, fuzzy, align, standards, performance) |
Voice_Biometric/ |
18 files (engine, ui, features, vad, embed_onnx, embed_wavlm, matcher, antispoof, liveness-adjacent, template_protection, crypto, registry, standards, performance, microphone, provenance, capture_worklet, audio_embed, c2pa_audio_hash) |
Iris_Biometric/ |
11 files (engine, matcher, quality, quality_full, liveness, camera, storage, ui, standards, template_protection, performance) |
Converter/ |
converter.js, ffmpeg-worker.js, ffmpeg.min.js |
Assistant/ |
assistant.js, assistant_data.js |
Decentralized_Identity_DID/ |
did.js |
Removal_Tools/ |
removal_tools.js (local-only placeholder; directory is gitignored) |
.tools/Developer_Toolkit/ |
eslint.config.mjs, commitlint.config.mjs, madge.config.js, workbox-config.js, lighthouserc.js, stryker.conf.js |
scripts/ |
fix-orphan-labels.js, build-search-index.js, e2e-coverage-guard.js, sync-i18n-json-to-js.js, translate-i18n.js |
vendor/ |
jspdf.umd.min.js, qrious.min.js, jszip.min.js, docx.umd.min.js, opentimestamps.min.js, pdf-lib.min.js |
The 4 root stylesheets (Style/style.css, Style/rtl.css, Style/responsive.css, Style/music-player.css) plus css/style.css and css/responsive.css for each of the 23 MPA pages (4 + 46 = 50).
index.html — SPA entry point404.html — Custom error pageStyle/index.html — Style directory entryStyle/pages/_page_template.html — MPA page templateStyle/pages/{name}/index.htmlAll GitHub workflow files and configuration files are whitelisted individually (64 entries; the repository runs 77 workflows in .github/workflows/ — related YML files such as Dependabot config are also covered).
package.json, package-lock.json, 7 toolkit configs (.markdownlint.json, .stylelintrc.json, backstop.json, biome.json, cspell.json, typedoc.json, .c8rc.json), 8 language files under Style/lang/ (ar, de, en, es, fr, ja, ko, zh), and Style/pages/search/search-index.json.
A dedicated MD_WHITELIST (5 entries) and XML_WHITELIST (1 entry) gate Markdown and XML fetches the same way.
Cross-origin requests are only allowed for specific, version-pinned CDN URLs:
huggingface.co, us.aws.cdn.hf.co, cdn-lfs.hf.coThe SW scans URL paths for embedded URLs at three encoding levels:
Layer 1 — Plain URLs: Detects :// in the URL path.
Layer 2 — Single-encoded URLs: Detects %3a%2f%2f (URL-encoded ://) and %2f%2f (URL-encoded //).
Layer 3 — Double-encoded URLs: Applies decodeURIComponent() twice to detect triple-encoded patterns like %253a%252f%252f.
After decoding, a regex checks for //domain. patterns (protocol-relative URLs with a dot after //).
Navigations to /RedoSan-Authenticity/logo.png, /RedoSan-Authenticity/logo-black.png, or /RedoSan-Authenticity/logo.webp are blocked with 403 on navigation requests. The logos are only displayed via CSS background-image on <div> elements, preventing direct URL access.
validateFileInput(inputElement)Defined in Style/shared_validation.js. The function is async and receives the <input> element (not a File). It runs these stages in order:
isDangerousFile() against BLOCKED_EXTS (55 entries, includes .exe….apk plus .js, .pyc, .epub, .xps, .oxps, .lnk, .svg, .svgz)isEnglishFilename() guards against RTL-override / homoglyph filename tricksdetectDangerousMagic() reads the header of files without an extensionmatchesAccept() compares the file against the input's accept listmatchesMagicBytes() verifies header bytes for PNG, JPEG, GIF, WebP, BMP, TIFF, PDF, MPEG, WAV, FLAC, OGG, MP4, WebM, AVIcheckDangerousContent() scans the first 4096 bytes for script tags, event handlers, javascript: and dangerous data URIscheckFileStructure() verifies PNG IEND / JPEG EOI markers in the file tailMAGIC_BYTES, DANGEROUS_PATTERNS, DOC_THREAT_PATTERNS, and DANGEROUS_MAGIC are exported alongside the validator.
checkDocumentThreats()(PDF/JavaScript,/JS,/OpenActiondetection) exists in both runtimes but is invoked by dedicated PDF flows — it is not part of the default validation pipeline.
validateFile(filePath, options)Defined in cli/utils.js:
isDangerousExt() against the CLI's BLOCKED_EXTS; throws Blocked dangerous file type: ...hasDangerousMagic() (first 64 bytes)checkMagicBytes() verifies declared type against actual contenthasDangerousContent() for image extensions (.png, .jpg, .jpeg, .gif, .webp, .bmp, .tiff, .tif, .svg)checkFileStructure() verifies container integrityEvery stage except the existence check can be bypassed with the global --allow-dangerous flag.
Browser JavaScript files start with an IIFE that validates window.location.href against:
/^https?:\/\/(.*\.)?(redo-san\.github\.io|localhost|127\.0\.0\.1)(:\d+)?(\/|$)/
The check is present in Style/shared.js, Style/shared_validation.js, Style/navigation.js, Watermark/watermark.js, C2PA/c2pa.js, C2PA/cbor.js, Pixel_Injection/*, and the biometric modules. file: URLs are exempt so local file access keeps working.
Unlike earlier releases, a failed check throws rather than silently no-ops:
throw new Error("RedoSan Authenticity: This script is protected by GPL license.");
This makes cloned deployments on untrusted domains fail loudly instead of appearing functional. The regex allows:
https://redo-san.github.io/ (production)https://*.redo-san.github.io/ (subdomains)http://localhost/ and http://127.0.0.1/ (development), any port, any pathIn the CLI, window is polyfilled without location, so the check is skipped.
sanitizeHtml() FunctionAll user-facing text that may contain HTML is sanitized through sanitizeHtml() (Style/i18n.js):
Tag whitelist approach (not regex removal):
var safeTags = /<\/?(h[23]|p|ul|li|a(?: href)?|br|strong|em|b|i|code|pre|blockquote|ol|span|div)\b[^>]*>/gi;
Only tags in the whitelist survive. All other tags are stripped.
Event handler removal (on whitelisted tags):
var eventPattern = /\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi;
JavaScript URL removal from href attributes.
escHtml() FunctionFor text content (no HTML intended), escHtml() (Style/shared.js) encodes:
| Character | Encoding |
|---|---|
< |
< |
> |
> |
& |
& |
" |
" |
' |
' |
Used in downloadBlob() (attrSafe), pixel-injection detection results, Service Worker error messages, and every setOutput() path.
pixel_injection.js wraps JSON.stringify(result) in escHtml() before assigning innerHTML, preventing XSS via crafted extraction results.
getAdvancedOptions() filters prototype pollution keys case-insensitively in both Pixel_Injection/pixel_injection.js and Pixel_Injection/watermark_advanced_ui.js:
var lower = key.toLowerCase();
if (lower === "__proto__" || lower === "constructor" || lower === "prototype") continue;
Numeric option values are additionally NaN-checked before use.
parseInt() returns NaN for non-numeric input, and NaN <= 0 is false, which would bypass a naive bounds check. Verified guards:
// cli/commands/watermark.js — data length
if (isNaN(dlen) || dlen <= 0 || dlen > 100_000) throw new Error("...");
// cli/commands/audio_watermark.js
if (isNaN(dlen) || dlen <= 0 || dlen > 5000) throw new Error("...");
// Watermark/watermark.js — bit-string extraction
if (dlen <= 0 || dlen > 100_000) return { ok: false, error: "..." };
| 0 Truncation (CVE-analog H2)Hash key generation previously used signed 32-bit truncation. It now reads the first 4 bytes as an unsigned 32-bit integer:
// Style/shared.js
function unpack32(b) {
return ((b[0] << 24) >>> 0) | (b[1] << 16) | (b[2] << 8) | b[3];
}
pack32() (shared.js:309) is the inverse. unpack32() values can never be negative, so they are safe as loop bounds or array indices.
Parsing of binary and hex strings always supplies an explicit radix (parseInt(bits, 2), Number.parseInt(hex, 16)). Reads of DOM <select> values (decimal option indices) rely on the default radix 10.
Pixel-injection extraction previously aborted at the first null byte (charCode === 0), letting an attacker truncate an extracted message. A repository-wide scan finds no remaining charCode === 0 break patterns in extraction code — bytes (including nulls) are read to the declared length.
WatermarkCore.prototype.extractDWT retains one deliberate fallback: after parseDecodedMessage() fails, a trailing "\0" trim (str.indexOf("\0")) strips zero-padding from the decoded string. The trim only runs on the fully decoded result, never mid-extraction.
Object URLs are revoked before being overwritten (or on teardown) across the app:
| Location | Pattern |
|---|---|
C2PA/c2pa.js:985 |
if (window._c2paSignedUrl) URL.revokeObjectURL(window._c2paSignedUrl) |
Watermark/watermark.js:481 |
revokes wmLastBlobUrl before creating a new one |
Certificate/*.js |
revoke after every PDF/DOCX/EPUB download |
Converter/converter.js |
revokes after each conversion (13 sites) |
Style/shared.js |
downloadBlob() revokes the previous URL |
clearSimpleData() (Style/simplified_countries.js) |
iterates all stored result URLs and revokes them |
runC2paStep() (Style/simplified.js) no longer dispatches redundant change events on the file input — validation happens once at upload time, so C2PA cannot sign an image while watermark/pixel-injection is still running. Additional guards:
simpleEnsureSection("c2pa")) to prevent parallel signings/^https?:\/\/[^\s/$.?#].[^\s]*$/i before signing(Other wizard steps — pixel injection, timestamp, fingerprint — still dispatch synthetic change events, but only to populate hidden professional-mode form fields.)
The browser UI includes detection for automated browsers:
checkAutomation() — Scores automation signals (0-100; automated at ≥ 40)showBotOverlay() — Displays a warning overlay when automation is detectednavigator.webdriver, document.documentElement[webdriver], legacy automation globals (callPhantom, _phantom, __nightmare), empty navigator.plugins, missing navigator.languages, UA/platform mismatch, implausible screen resolution, and chrome.runtime without an extension ID (headless Chrome). Backstop test runs are exempted.detectWebRTCIPs() — Uses WebRTC to detect local/public IP mismatchstartAsyncVPNDetection() — Asynchronous VPN/proxy detection with timeout| Field | Rule |
|---|---|
| Name | maxlength="100" |
| Phone | Digits-only (non-digits stripped with warning), maxlength="15", per-country length via COUNTRY_CODES[n].len; auto-detection chain: Intl.NumberFormat → DateTimeFormat → Collator → navigator.languages → timezone map → fallback |
maxlength="64", regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/, red warning on invalid input, re-validated in simpleNext() |
|
| Website | maxlength="200", auto-prefixed with https:// (prefixHttps()), regex /^https?:\/\/[^\s/$.?#].[^\s]*$/i |
| Social / music / C2PA links | maxlength="80", same URL regex, per-field warning span, re-validated at C2PA submission |
All validators live in Style/simplified_countries.js (validateEmailInput, validatePhoneInput, validateUrlInput, validateSocialInput, prefixHttps).
Face, voice, and iris biometrics process everything in the browser:
face_registry.js), voice registry (voice_registry.js), and iris templates (iris_storage.js, IndexedDB) never leave the machine; only CDN/Hugging Face model files (ArcFace, ECAPA/WavLM, ONNX runtimes) are downloadedface_webauthn.js); voice and iris use dedicated template-protection modules (voice_crypto.js / voice_template_protection.js, iris_crypto-style protection in iris_template_protection.js, aligned with ISO/IEC 24745)faceConsent* / voiceConsent* handlers require explicit, withdrawable consent before registration or matching*_antispoof.js, *_liveness.js)getUserMedia / WebAudio / canvas paths; nothing is sent to a server| Measure | Detail |
|---|---|
| Two-tier npm audit | CI unit job runs a blocking npm audit --omit=dev --audit-level=high (audit:prod); the E2E job runs a full-scope npm audit as non-blocking; security-audit.yml re-runs weekly (Monday 03:00 UTC) with an advisory-exceptions policy gate |
| Dependabot | Weekly updates in 4 groups: dev-dependencies, production-dependencies, security (applies-to: security-updates), github-actions |
| Overrides (22) | tar 7.5.22, js-yaml 4.3.2, markdown-it 14.3.2, smol-toml 1.9.0, qs 6.16.0, katex 0.18.2, serialize-javascript 7.0.6, uuid 14.0.0, glob 13.0.6, got 11.8.5, tmp 0.2.7, nanoid, brace-expansion 5.0.12, diff >=8.0.3, @mapbox/node-pre-gyp 2.0.3, @puppeteer/browsers 3.2.1, @babel/core 7.29.7, colord 2.9.4, ip-address 10.7.3, basic-ftp 6.2.1, fast-uri 3.1.8 |
| Password derivation | crypto.pbkdf2Sync with 100,000 iterations |
| No external secrets | C2PA test key is embedded (well-known, not for production) |
| CDN pinning | All CDN URLs are version-pinned and SW-whitelisted; SRI integrity attributes are not currently applied to lazy-loaded CDN scripts — prefer the local vendor/ copies, which load first |
| Metric | Status |
|---|---|
| Unit tests | 107 test files, 6,200+ assertions passing (Node 22 + 24 matrix) |
| E2E tests | 59 Playwright suites |
| CI/CD workflows | 77 workflows in .github/workflows/ |
| Security scanning | CodeQL, Gitleaks/secret scanning, Semgrep, Trufflehog, Scorecards, zizmor run as dedicated workflows; 30+ historical CodeQL alerts resolved |
| Local gates | Husky pre-commit (git diff --check, large-file guard, secret scan, lint-staged) and pre-push (commitlint, actionlint) |
| Linting suppression | // codeql[js/rule-id] format for intentional code patterns |
Wiki: API Reference
Wiki: Architecture
Wiki: FAQ
Wiki: Face Biometric
Wiki: Home
Wiki: Installation
Wiki: Iris Biometric
Wiki: Voice Biometric
Wiki: _Sidebar