STEGO·STUDIO is an experimental, human-directed AI software project — not a certified professional forensic, security, or steganalysis product. Do not rely on it as the sole basis for critical security or forensic decisions. The project context and development methodology are described in README.md.
This tool exists to open images you have reason to distrust. Every byte read from a file is treated as hostile — metadata, decoded payloads, structural fields. Anything drawn from a file and shown on screen is escaped before it reaches the page.
Everything runs in your browser. Nothing is uploaded, and the build refuses to
emit a file that would contact the network at runtime. The distributed HTML also
contains a restrictive Content Security Policy: connect-src 'none' blocks
script-initiated network connections at runtime, executable inline scripts are
authorized by build-time SHA-256 hashes, and general JavaScript eval is not
allowed. The Argon2 bundle receives the narrower 'wasm-unsafe-eval' permission
needed to instantiate its embedded WebAssembly.
These are design limits, not oversights. Stating them plainly is more useful than implying coverage that does not exist.
-
The CSP is defense in depth, not a complete browser sandbox. It is delivered inside the standalone HTML with a
<meta http-equiv>policy so the same artifact works when opened directly from disk. Meta-delivered CSP cannot enforce directives such asframe-ancestors, reporting, or CSPsandbox; those require response headers or are unavailable in meta policies. The current UI also uses inline style attributes and CSSOM updates extensively, sostyle-src 'unsafe-inline'remains a deliberate compatibility allowance. Script execution is still hash-pinned andscript-src-attr 'none'keeps inline event handlers disabled.img-src 'self' data: blob:is also deliberate. The deployed favicon and touch-icon files are real compatibility resources (including Android/high-density scenarios), and theirdata:behaviour has not been verified across all browser/home-screen flows, especially Safari/iOS. Keeping'self'means that, if hostile script were already executing, an image request to the same origin could still carry data in its path or query string.connect-src 'none'does not close that separate image channel. This residual channel is documented rather than hidden; removing'self'should wait for browser/device compatibility tests, not assumption. Top-level navigation is a separate residual channel as well: if hostile script were already executing, assigninglocationto an external URL could carry data through the navigation itself.connect-src 'none'does not govern top-level navigation, and current target browsers do not provide a deployed CSP directive that closes that path from a meta policy. The CSP therefore must not be cited as a complete no-exfiltration sandbox. -
Carriers are not sanitized. The Encoder overwrites the positions its new payload needs and leaves the rest of the image unchanged. Reusing an image that already contained hidden data can therefore preserve traces of the earlier payload; an earlier unencrypted message may remain partly readable without any password. Encode from an original cover when prior hidden data must not be carried forward. On lossless carriers, Carrier Preflight performs a lightweight password-free check for obvious native headers and coherent readable text in common pixel-LSB layouts before Encode. It can miss password-concealed, non-textual data without a visible native header, adaptive, or unsupported hidden data; a quiet preflight is not evidence that the carrier contains no hidden content.
-
Content-adaptive steganography is not reliably detected by the built-in statistical analysis. HILL, UNIWARD, J-UNIWARD and similar may evade the signals used here, and LSB Matching can also avoid the structural patterns associated with LSB Replacement. Specialised detection generally uses trained models. Those models can run in browsers through WASM/WebGPU; this offline single-file build deliberately does not include them. Finding nothing here is not evidence that an image is clean.
-
C2PA signatures are not cryptographically verified. The tool detects a manifest in the container the standard mandates and reads the declaration inside it. It does not check the signature, the certificate chain, or whether the content still matches what was signed. A declaration can be copied from a genuine file into another one.
-
EXIF is unauthenticated. Camera fields are ordinary text that any program can write. They count as supporting evidence, never as proof of provenance.
-
Password-protected lossless writes use strong, domain-separated structural keys. New PNG/lossless payloads written with a password use a fresh 128-bit structural salt, Argon2id, and separate HKDF-derived keys for header protection, header authentication, body order (where applicable), and AES-256-GCM content encryption. The protected header is authenticated before its mode or length is trusted. Older lossless formats remain readable, and writes without a password keep the compatible unprotected format.
This removes the old 32-bit structural ceiling from the main lossless password-protected path. It does not turn a weak password into 256 bits of security. Running any amount of key-stretching over
123456still leaves you with the guessability of123456; Argon2id makes each guess expensive, which buys time, not entropy. Everything here remains bounded by the password you choose. -
The robust JPEG path still has an independent 32-bit structural seed. Its content encryption is not being described as 32-bit; the remaining limitation is the password-derived slot/dither plan used by that separate recompression- resistant format. Migrating it safely requires a JPEG-specific design that preserves ECC/QIM/recompression behaviour, so it is intentionally outside the lossless-format change above.
-
Structured recovery of legacy LSB frames is not cryptographic authentication. For recognized historical framings such as
JOI_LSBand the observedStegv1 layout, the Decoder can validate the declared byte range and require that exact payload to decode as UTF-8 before treating it as direct structured recovery. That is stronger than a sliding-window text guess, but the framing itself has no MAC or signature: it can be forged by anyone who can write those LSBs. -
Third-party extraction is partial. OpenStego encrypted payloads are identified but not decrypted. A narrow Stegano-compatible PNG path reads the measured v3 byte wire only with the default/identity pixel order, strict UTF-8 and at least 12 payload bytes; the framing has no authentication magic, so recovery confirms compatible hidden content rather than authorship by Stegano. Legacy 2.5 Unicode semantics, UTF-32LE, generators and shift are outside that automatic path. LSBSteg-compatible binary framing is automatic only for exact PNG pixels: its 64-bit big-endian length is handled as
BigInt, must fit the physical carrier, and is capped at 32 MiB before body allocation. Because that wire has no magic/checksum/authentication, successful recovery proves compatible hidden bytes, not authorship by LSBSteg. Cloacked Pixel-compatible recovery is limited to exact PNG pixels with a supplied password and uses a shared 32 MiB cap before body allocation. Its SHA-256 + AES-256-CBC format has no MAC or authentication tag, so valid padding alone does not authenticate the password; the Decoder requires strict pad32 plus a second strong text/file-signature gate before publishing recovered content, and reports only compatible framing. wbStego4open-compatible recovery is automatic only for raw uncompressed 24-bit BMP. The decoder reads the physical BMP raster bytes directly rather than canvas/ImageData. The plaintext 4.x framing has no strong authentication magic, so a plausible 24-bit length/extension is never enough: STEGO·STUDIO requires at least 12 bytes of strict readable UTF-8 or a recognized binary file signature before publishing recovered content. The protected 4.x header marker can identify Blowfish, Twofish, CAST128 or Rijndael, but those ciphers are not decrypted in this first route. 4/8-bit and RLE BMP, legacy 2.x/3.x control/mix modes, and TXT/HTML/PDF carriers remain outside scope. Recovery/header identification proves compatible structure, not authorship by wbStego. The unencrypted Stegpy-compatiblestegv3path is automatic in exact PNG pixels and JPEG DCT because its 48-bit magic provides a strong fail-fast; password/Fernet variants are not attempted because encryption hides that magic. BMP/WebP/GIF/WAV Stegpy carriers remain outside the current exact-I/O boundary. Stegify v1.2 single-carrier is available only through the explicit Advanced format scan: its header contains a length but no strong magic, checksum or authentication, so a structurally plausible result is never promoted to tool attribution. The explicit scan validates size before reading the full body and caps extraction at 32 MB to avoid hostile memory requests. The same opt-in surface also includes a constrained expanded LSB layout scan whose extraction family is validated against zsteg 0.2.14 semantics. Because that family has no framing at all, a passing UTF-8/readability/entropy gate is labelled only as a candidate and never feeds Threat or tool attribution. The public candidate gate is deliberately precision-first (32+ readable characters beginning within the first 8 extracted bytes) after alpha/antialias and smooth-gradient false-candidate studies. Full zsteg-style--allsearch and generic unframed binary recovery are deliberately outside scope. The SilentEye 0.4.1 JPEG is attempted automatically only for the measured default profile (k=5, bottom header, blank/default passphrase, no encryption). Its 32-bit header is only a length, so length plausibility alone is never treated as proof: qCompress/framing must validate terminal text or a framed file before anything is published. Custom k/header/passphrase, AES and non-JPEG carriers remain outside this first route. The Luke/Jsteg CLI JPEG framing is read, but that does not imply runtime support for the different classic Derek Upham JSteg wire. JPHide/JPSeek support is limited to the historical v0.5/0.51 JPEG wire with a supplied ASCII password. The first runtime route deliberately refuses non-ASCII JPHide passwords because the old platform-specific byte encoding has not been proven; it does not silently substitute modern UTF-8. Within that boundary: mode 0 content is extracted, while mode 1 stops at a password/header-verified state because its final content transform is not implemented. Linux v0.3 is a different wire and is not claimed as compatible. The JPHide route reuses the shared DCT decode, verifies the redundant Blowfish-derived header before content publication, and applies a 32 MiB extraction cap. The classic wire/oracle has been validated separately, yet remains on hold because 9/24 measured negative JPEGs passed its weak length-plausibility gate, including a clean historicalcjpegcontrol. Steghide covers eight practical pairs (no encryption plus rijndael-128 in all seven measured modes) out of roughly 129 cipher/mode combinations, and its BMP path is not integrated. Full matrix, with the validation basis for each row: docs/COMPATIBILITY.md. -
Plausible-deniability reporting intentionally suppresses layer identity after a successful native extraction. The implementation knows whether the payload was recovered from the ordinary native route or from the alternative tail layer, but once either route authenticates and yields a message the public Analyzer/JSON uses the same
nativeExtractedevidence class. This is deliberate: exporting the route itself would become a distinguisher between two valid passwords. It does not hide whether a STEGO·STUDIO payload was recovered; it hides only which internal layer produced that authenticated recovery.
While an analysis is running, inputs and controls that could change or re-render its state are deliberately locked. Each analysis also carries a generation token and works from a snapshot, so a result belonging to a superseded image is discarded rather than displayed.
Open an issue at https://github.com/rickschaves/stegostudio/issues
If you believe an issue should not be public before a fix ships, say so in a minimal issue without details and a private channel will be arranged.