diff --git a/.gitignore b/.gitignore index 3af7bf2..dec89e8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ node_modules/ # Local environment noise .npm/ npm-debug.log* +.DS_Store +**/.DS_Store + +# Operator ICE override holds TURN credentials — never commit it. +# Use config/ice-servers.example.js as the template. +config/ice-servers.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a79223..96dc99a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v4.8.9 — Security hardening patch + +This release closes a vulnerable dependency, removes committed TURN credentials, and tightens production logging. + +### Security + +- Upgraded DOMPurify from 3.4.4 to a patched release, resolving a high-severity XSS advisory (GHSA-87xg-pxx2-7hvx) in the incoming-message sanitizer. +- Upgraded the `esbuild` build dependency to clear a high-severity advisory in the toolchain. `npm audit` now reports zero vulnerabilities. +- Stopped tracking `config/ice-servers.js` (operator TURN credentials) in Git and added `config/ice-servers.example.js` as a template. Operators must rotate any previously committed credentials. +- Removed temporary debug branches from the production logger so it no longer prints error context or info/debug payloads — only an opaque error code. + +### Documentation + +- Updated the supported-release table in `SECURITY.md` to the v4.8.x line. +- Synchronized the version string across the header, manifest, README, and in-app initialization message. + ## v4.8.8 — File transfer consent fix This patch completes the mandatory receiver-consent gate for incoming file transfers and resolves a callback ownership conflict that caused every incoming file request to be silently auto-rejected. diff --git a/README.md b/README.md index ef2a178..3597e3a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SecureBit.chat v4.8.7 +# SecureBit.chat v4.8.9 SecureBit.chat is a browser-based peer-to-peer chat application built on WebRTC and Web Crypto APIs. It is designed for direct encrypted communication, explicit peer verification, and a small operational footprint without account registration or server-side message storage. @@ -15,13 +15,17 @@ SecureBit.chat uses: A session is not treated as verified until both peers complete the interactive SAS flow. Each user must compare the displayed code with the peer through an out-of-band channel and enter the matching code manually. Three failed SAS attempts terminate the session. -## Highlights in v4.8.7 +## Highlights in v4.8.9 -- Manual WebRTC setup now preserves pending offer/answer state during slow out-of-band exchange. +- Patched a high-severity XSS advisory in the DOMPurify dependency (the message sanitizer) by upgrading to a fixed release. +- Operator TURN credentials are no longer committed to the repository; use `config/ice-servers.example.js` as a template. +- The production logger no longer prints error context or info/debug output, only opaque error codes. + +This patch release builds on the earlier hardening pass: + +- Manual WebRTC setup preserves pending offer/answer state during slow out-of-band exchange. - TURN relay fallback can be configured through `config/ice-servers.js` for restrictive networks. -- ICE diagnostics now identify mDNS-only candidate failures without exposing full peer IPs. - -This patch release strengthens the existing security model with a focused hardening pass: +- ICE diagnostics identify mDNS-only candidate failures without exposing full peer IPs. - SAS verification is bound to the actual DTLS fingerprint strings of both peers - chat sanitization uses DOMPurify-backed text-only output diff --git a/SECURITY.md b/SECURITY.md index 35d651b..fd70247 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,8 @@ | Release | Status | Protocol | | --- | --- | --- | -| v4.1.x | Supported | 4.1 | +| v4.8.x | Supported | 4.1 | +| v4.1.x – v4.7.x | Unsupported | 4.1 | | earlier releases | Unsupported | legacy | Users should run the current supported release line to receive the latest verification, storage, and file-transfer protections. diff --git a/SECURITY_DISCLAIMER.md b/SECURITY_DISCLAIMER.md index 25d3a45..98e4c83 100644 --- a/SECURITY_DISCLAIMER.md +++ b/SECURITY_DISCLAIMER.md @@ -22,6 +22,6 @@ SecureBit.chat is intended for legitimate private communication, journalism, res ## Current release -- Product release: `v4.8.5` +- Product release: `v4.8.9` - Protocol version: `4.1` - Last updated: May 17, 2026 diff --git a/config/ice-servers.example.js b/config/ice-servers.example.js new file mode 100644 index 0000000..8b490ad --- /dev/null +++ b/config/ice-servers.example.js @@ -0,0 +1,21 @@ +// SecureBit.chat operator ICE server override — TEMPLATE. +// +// Copy this file to `config/ice-servers.js` and fill in your own TURN/STUN +// servers. The real `config/ice-servers.js` is git-ignored on purpose: +// TURN credentials are visible to every browser that loads the page, so they +// must never be committed to a public repository. Rotate them from your TURN +// provider dashboard if they are ever exposed. +// +// If this override is absent, the WebRTC manager falls back to the built-in +// public STUN defaults (standard mode only — no relay/IP protection). +window.SECUREBIT_ICE_SERVERS = [ + { urls: 'stun:stun.cloudflare.com:3478' }, + { + urls: [ + 'turn:YOUR_TURN_HOST:3478?transport=udp', + 'turn:YOUR_TURN_HOST:3478?transport=tcp' + ], + username: 'YOUR_TURN_USERNAME', + credential: 'YOUR_TURN_CREDENTIAL' + } +]; diff --git a/config/ice-servers.js b/config/ice-servers.js deleted file mode 100644 index b9e34f1..0000000 --- a/config/ice-servers.js +++ /dev/null @@ -1,15 +0,0 @@ -// SecureBit.chat operator ICE server override. -// Loaded before the WebRTC manager is created. Credentials are visible to browsers; -// rotate them from the ExpressTURN dashboard if this file is published publicly. -window.SECUREBIT_ICE_SERVERS = [ - { urls: 'stun:stun.cloudflare.com:3478' }, - { urls: 'stun:stun.expressturn.com:3478' }, - { - urls: [ - 'turn:free.expressturn.com:3478?transport=udp', - 'turn:free.expressturn.com:3478?transport=tcp' - ], - username: '000000002094555952', - credential: 't1oK9Zftes9j7E7hJmsLad9jq1M=' - } -]; diff --git a/dist/app-boot.js b/dist/app-boot.js index 7eb841b..483da5c 100644 --- a/dist/app-boot.js +++ b/dist/app-boot.js @@ -5,7 +5,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + try { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + } catch (e) { + throw mod = 0, e; + } }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { @@ -954,7 +958,7 @@ function isRegex(value) { return false; } } -var html$1 = freeze(["a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "search", "section", "select", "selectedcontent", "shadow", "slot", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]); +var html$1 = freeze(["a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "search", "section", "select", "shadow", "slot", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]); var svg$1 = freeze(["svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "enterkeyhint", "exportparts", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "inputmode", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "part", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "style", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "view", "vkern"]); var svgFilters = freeze(["feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence"]); var svgDisallowed = freeze(["animate", "color-profile", "cursor", "discard", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "hatch", "hatchpath", "mesh", "meshgradient", "meshpatch", "meshrow", "missing-glyph", "script", "set", "solidcolor", "unknown", "use"]); @@ -981,13 +985,26 @@ var ATTR_WHITESPACE = seal( ); var DOCTYPE_NAME = seal(/^html$/i); var CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i); +var ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g); +var COMMENT_MARKUP_PROBE = seal(/<[/\w]/g); +var FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i); +var SELF_CLOSING_TAG = seal(/\/>/i); var NODE_TYPE = { element: 1, + attribute: 2, text: 3, + cdataSection: 4, + entityReference: 5, // Deprecated - progressingInstruction: 7, + entityNode: 6, + // Deprecated + processingInstruction: 7, comment: 8, - document: 9 + document: 9, + documentType: 10, + documentFragment: 11, + notation: 12 + // Deprecated }; var getGlobal = function getGlobal2() { return typeof window === "undefined" ? null : window; @@ -1029,10 +1046,13 @@ var _createHooksMap = function _createHooksMap2() { uponSanitizeShadowNode: [] }; }; +var _resolveSetOption = function _resolveSetOption2(cfg, key, fallback, options) { + return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback; +}; function createDOMPurify() { let window2 = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal(); const DOMPurify = (root) => createDOMPurify(root); - DOMPurify.version = "3.4.4"; + DOMPurify.version = "3.4.10"; DOMPurify.removed = []; if (!window2 || !window2.document || window2.document.nodeType !== NODE_TYPE.document || !window2.Element) { DOMPurify.isSupported = false; @@ -1041,14 +1061,21 @@ function createDOMPurify() { let document2 = window2.document; const originalDocument = document2; const currentScript = originalDocument.currentScript; - const DocumentFragment = window2.DocumentFragment, HTMLTemplateElement = window2.HTMLTemplateElement, Node = window2.Node, Element = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap, NamedNodeMap = _window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap, HTMLFormElement = window2.HTMLFormElement, DOMParser = window2.DOMParser, trustedTypes = window2.trustedTypes; + window2.DocumentFragment; + const HTMLTemplateElement = window2.HTMLTemplateElement, Node = window2.Node, Element = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap; + _window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap; + window2.HTMLFormElement; + const DOMParser = window2.DOMParser, trustedTypes = window2.trustedTypes; const ElementPrototype = Element.prototype; const cloneNode = lookupGetter(ElementPrototype, "cloneNode"); const remove = lookupGetter(ElementPrototype, "remove"); const getNextSibling = lookupGetter(ElementPrototype, "nextSibling"); const getChildNodes = lookupGetter(ElementPrototype, "childNodes"); const getParentNode = lookupGetter(ElementPrototype, "parentNode"); + const getShadowRoot = lookupGetter(ElementPrototype, "shadowRoot"); + const getAttributes = lookupGetter(ElementPrototype, "attributes"); const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeType") : null; + const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeName") : null; if (typeof HTMLTemplateElement === "function") { const template = document2.createElement("template"); if (template.content && template.content.ownerDocument) { @@ -1057,6 +1084,39 @@ function createDOMPurify() { } let trustedTypesPolicy; let emptyHTML = ""; + let defaultTrustedTypesPolicy; + let defaultTrustedTypesPolicyResolved = false; + let IN_TRUSTED_TYPES_POLICY = 0; + const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy2() { + if (IN_TRUSTED_TYPES_POLICY > 0) { + throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.'); + } + }; + const _createTrustedHTML = function _createTrustedHTML2(html2) { + _assertNotInTrustedTypesPolicy(); + IN_TRUSTED_TYPES_POLICY++; + try { + return trustedTypesPolicy.createHTML(html2); + } finally { + IN_TRUSTED_TYPES_POLICY--; + } + }; + const _createTrustedScriptURL = function _createTrustedScriptURL2(scriptUrl) { + _assertNotInTrustedTypesPolicy(); + IN_TRUSTED_TYPES_POLICY++; + try { + return trustedTypesPolicy.createScriptURL(scriptUrl); + } finally { + IN_TRUSTED_TYPES_POLICY--; + } + }; + const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy2() { + if (!defaultTrustedTypesPolicyResolved) { + defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); + defaultTrustedTypesPolicyResolved = true; + } + return defaultTrustedTypesPolicy; + }; const _document = document2, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName; const importNode = originalDocument.importNode; let hooks = _createHooksMap(); @@ -1122,7 +1182,43 @@ function createDOMPurify() { let IN_PLACE = false; let USE_PROFILES = {}; let FORBID_CONTENTS = null; - const DEFAULT_FORBID_CONTENTS = addToSet({}, ["annotation-xml", "audio", "colgroup", "desc", "foreignobject", "head", "iframe", "math", "mi", "mn", "mo", "ms", "mtext", "noembed", "noframes", "noscript", "plaintext", "script", "style", "svg", "template", "thead", "title", "video", "xmp"]); + const DEFAULT_FORBID_CONTENTS = addToSet({}, [ + "annotation-xml", + "audio", + "colgroup", + "desc", + "foreignobject", + "head", + "iframe", + "math", + "mi", + "mn", + "mo", + "ms", + "mtext", + "noembed", + "noframes", + "noscript", + "plaintext", + "script", + // mirrors the selected