Menu ▾ ▴

Security

RedoSan

Security Architecture

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.


1. Service Worker Security Layer

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.

1.1 Dangerous Extension Blocklist (61 Extensions)

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.

1.2 JavaScript Whitelist (128 Entries / 126 Unique)

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

1.3 CSS Whitelist (50 Files)

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).

1.4 HTML Whitelist (27 Files)

  • index.html — SPA entry point
  • 404.html — Custom error page
  • Style/index.html — Style directory entry
  • Style/pages/_page_template.html — MPA page template
  • 23 standalone MPA pages under Style/pages/{name}/index.html

1.5 YAML Whitelist (64 Files)

All 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).

1.6 JSON Whitelist (18 Files)

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.

1.7 Markdown and XML Whitelists (5 + 1)

A dedicated MD_WHITELIST (5 entries) and XML_WHITELIST (1 entry) gate Markdown and XML fetches the same way.

1.8 External CDN Whitelist (23 URLs)

Cross-origin requests are only allowed for specific, version-pinned CDN URLs:

  • jsPDF 2.5.1, docx 8.5.0, QRious 4.0.2, JSZip 3.10.1 — 3 mirrors each (jsDelivr, unpkg, cdnjs) = 12
  • LameJS 1.2.1 (1), FFmpeg core 0.11.1 / 0.12.1 (2)
  • C2PA Web 0.8.1 — ESM bundle + WASM resource (2)
  • @vladmandic/human 3.3.6 (1), TensorFlow.js WASM backend (1), onnxruntime-web 1.20.1 (1)
  • Hugging Face model hosts (3): huggingface.co, us.aws.cdn.hf.co, cdn-lfs.hf.co

1.9 Embedded URL Detection (3 Layers)

The 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.


2. Multi-Stage File Validation

2.1 Browser: 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:

  1. Extension blocklist — isDangerousFile() against BLOCKED_EXTS (55 entries, includes .exe….apk plus .js, .pyc, .epub, .xps, .oxps, .lnk, .svg, .svgz)
  2. Size cap — files larger than 200 MB are rejected
  3. Filename check — isEnglishFilename() guards against RTL-override / homoglyph filename tricks
  4. Extensionless magic sniff — detectDangerousMagic() reads the header of files without an extension
  5. Accept attribute — matchesAccept() compares the file against the input's accept list
  6. Magic bytes — matchesMagicBytes() verifies header bytes for PNG, JPEG, GIF, WebP, BMP, TIFF, PDF, MPEG, WAV, FLAC, OGG, MP4, WebM, AVI
  7. Content scan — checkDangerousContent() scans the first 4096 bytes for script tags, event handlers, javascript: and dangerous data URIs
  8. Structure check — checkFileStructure() verifies PNG IEND / JPEG EOI markers in the file tail

MAGIC_BYTES, DANGEROUS_PATTERNS, DOC_THREAT_PATTERNS, and DANGEROUS_MAGIC are exported alongside the validator.

checkDocumentThreats() (PDF /JavaScript, /JS, /OpenAction detection) exists in both runtimes but is invoked by dedicated PDF flows — it is not part of the default validation pipeline.

2.2 CLI: validateFile(filePath, options)

Defined in cli/utils.js:

  1. Extension blocklist — isDangerousExt() against the CLI's BLOCKED_EXTS; throws Blocked dangerous file type: ...
  2. Extensionless sniff — files without an extension are checked with hasDangerousMagic() (first 64 bytes)
  3. Magic bytes — checkMagicBytes() verifies declared type against actual content
  4. Content scan — hasDangerousContent() for image extensions (.png, .jpg, .jpeg, .gif, .webp, .bmp, .tiff, .tif, .svg)
  5. Structure check — checkFileStructure() verifies container integrity

Every stage except the existence check can be bypassed with the global --allow-dangerous flag.


3. Domain Origin Self-Check

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 path

In the CLI, window is polyfilled without location, so the check is skipped.


4. XSS Prevention

4.1 sanitizeHtml() Function

All 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.

4.2 escHtml() Function

For text content (no HTML intended), escHtml() (Style/shared.js) encodes:

Character Encoding
< &lt;
> &gt;
& &amp;
" &quot;
' &#39;

Used in downloadBlob() (attrSafe), pixel-injection detection results, Service Worker error messages, and every setOutput() path.

4.3 JSON Safe Rendering

pixel_injection.js wraps JSON.stringify(result) in escHtml() before assigning innerHTML, preventing XSS via crafted extraction results.


5. Prototype Pollution Prevention

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.


6. Integer Overflow / NaN Protection

6.1 NaN Bounds Check Bypass (CVE-analog C1)

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: "..." };

6.2 Key Collision via | 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.

6.3 parseInt Radix

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.


7. Null-Byte Handling (C3)

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.


8. Blob URL Leak Prevention

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

9. C2PA Signing Race Prevention

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:

  • The C2PA button is disabled while the module lazy-loads (simpleEnsureSection("c2pa")) to prevent parallel signings
  • Social/music links are re-validated against /^https?:\/\/[^\s/$.?#].[^\s]*$/i before signing
  • Simple-mode content-type checkboxes are synced into the hidden professional form before the manifest is built

(Other wizard steps — pixel injection, timestamp, fingerprint — still dispatch synthetic change events, but only to populate hidden professional-mode form fields.)


10. Bot / Automation Detection

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 detected
  • Detection methods: navigator.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.

11. VPN Detection

  • detectWebRTCIPs() — Uses WebRTC to detect local/public IP mismatch
  • startAsyncVPNDetection() — Asynchronous VPN/proxy detection with timeout
  • Results are displayed in the UI for user awareness, not used for blocking

12. Input Validation

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
Email 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).


13. Biometric Data Handling

Face, voice, and iris biometrics process everything in the browser:

  • On-device templates — face registry (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 downloaded
  • Encrypted at rest — face templates are protected by a WebAuthn PRF vault (face_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)
  • Session-scoped consent — faceConsent* / voiceConsent* handlers require explicit, withdrawable consent before registration or matching
  • Presentation-attack detection — anti-spoof and liveness modules in all three pipelines (*_antispoof.js, *_liveness.js)
  • No uploads — camera frames, microphone audio, and extracted descriptors are handled via local getUserMedia / WebAudio / canvas paths; nothing is sent to a server

14. Dependency Security

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

15. Code Quality

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


Related

Wiki: API Reference
Wiki: Architecture
Wiki: FAQ
Wiki: Face Biometric
Wiki: Home
Wiki: Installation
Wiki: Iris Biometric
Wiki: Voice Biometric
Wiki: _Sidebar