TheBug95 commited on
Commit
7f69fd6
·
1 Parent(s): 5979537

Fix: eliminate all iframes - use onerror JS injection for protection + beforeunload

Browse files
interface/components/image_protection.py CHANGED
@@ -3,44 +3,30 @@
3
  Injects CSS and JavaScript into the Streamlit page to prevent users from
4
  downloading, dragging, or otherwise saving the confidential medical images.
5
 
6
- KEY DESIGN DECISION:
7
  Streamlit's st.markdown(unsafe_allow_html=True) renders <style> tags but
8
- STRIPS <script> tags for security. Therefore:
9
- CSS protections injected via st.markdown (works natively).
10
- • JS protections injected via st.components.v1.html() which creates
11
- a real iframe where JavaScript executes. From that iframe we reach
12
- the main Streamlit page via window.parent.document (same-origin).
13
 
14
  Protection layers (defence-in-depth):
15
- 1. CSS: pointer-events:none, user-select:none, draggable:false on <img>.
16
- 2. CSS: transparent ::after overlay on stImage containers blocks
17
- right-click "Save image as…".
18
- 3. CSS: -webkit-touch-callout:none blocks mobile long-press save.
19
- 4. JS: contextmenu event blocked on the ENTIRE parent document.
20
- 5. JS: Ctrl+S / Ctrl+U / Ctrl+Shift+I / Ctrl+Shift+J / Ctrl+Shift+C /
21
- F12 all intercepted and cancelled.
22
- 6. JS: dragstart blocked for all images.
23
- 7. JS: MutationObserver re-applies draggable=false to dynamically added
24
- images (Streamlit re-renders on every interaction).
25
- 8. JS: Blob/URL revocation — monkey-patches URL.createObjectURL and
26
- document.createElement to block programmatic image extraction.
27
-
28
- IMPORTANT LIMITATION:
29
- No client-side measure can guarantee absolute prevention. A technically
30
- sophisticated user could still extract images through OS-level screenshots,
31
- network packet inspection, or browser extensions that bypass JS hooks.
32
- These protections eliminate ALL standard browser download paths and raise
33
- the bar significantly.
34
  """
35
 
36
  import streamlit as st
37
- import streamlit.components.v1 as components
38
 
39
- # ── CSS injected via st.markdown (Streamlit renders <style> natively) ────────
40
- _PROTECTION_CSS = """
41
  <style>
42
  /* Layer 1: Disable ALL interaction on <img> tags */
43
- img {
44
  pointer-events: none !important;
45
  user-select: none !important;
46
  -webkit-user-select: none !important;
@@ -69,117 +55,71 @@ img {
69
  -webkit-user-drag: none !important;
70
  user-drag: none !important;
71
  }
72
- </style>
73
- """
74
 
75
- # ── JavaScript injected via components.html (runs in real iframe) ────────────
76
- # From the iframe we access window.parent.document to attach listeners
77
- # on the ACTUAL Streamlit page, not just inside the hidden iframe.
78
- _PROTECTION_JS_HTML = """
79
- <script>
80
- (function () {
81
- // The parent document is the real Streamlit page
82
- var doc;
83
- try { doc = window.parent.document; } catch(e) { doc = document; }
84
-
85
- // Guard: only inject once per page lifecycle
86
- if (doc.__ophthalmo_protection__) return;
87
- doc.__ophthalmo_protection__ = true;
88
-
89
- function block(e) {
90
- e.preventDefault();
91
- e.stopPropagation();
92
- e.stopImmediatePropagation();
93
- return false;
94
- }
95
 
96
- // ── Layer 4: Block context menu on ENTIRE page ──────────────────────
97
- doc.addEventListener('contextmenu', function (e) {
98
- return block(e);
99
- }, true);
100
-
101
- // ── Layer 5: Block keyboard shortcuts ───────────────────────────────
102
- doc.addEventListener('keydown', function (e) {
103
- var dominated = false;
104
- var ctrl = e.ctrlKey || e.metaKey;
105
- var key = e.key ? e.key.toLowerCase() : '';
106
-
107
- // Ctrl+S — Save page
108
- if (ctrl && key === 's') dominated = true;
109
- // Ctrl+U — View source
110
- if (ctrl && key === 'u') dominated = true;
111
- // Ctrl+P — Print (can save as PDF with images)
112
- if (ctrl && key === 'p') dominated = true;
113
- // F12 — DevTools
114
- if (e.keyCode === 123) dominated = true;
115
- // Ctrl+Shift+I — DevTools (Inspector)
116
- if (ctrl && e.shiftKey && key === 'i') dominated = true;
117
- // Ctrl+Shift+J — DevTools (Console)
118
- if (ctrl && e.shiftKey && key === 'j') dominated = true;
119
- // Ctrl+Shift+C — DevTools (Element picker)
120
- if (ctrl && e.shiftKey && key === 'c') dominated = true;
121
-
122
- if (dominated) return block(e);
123
- }, true);
124
-
125
- // ── Layer 6: Block drag-and-drop of images ─────────────────────────
126
- doc.addEventListener('dragstart', function (e) {
127
- if (e.target && e.target.tagName === 'IMG') return block(e);
128
- }, true);
129
-
130
- // ── Layer 7: MutationObserver — lock new images as they appear ──────
131
- function lockImages(root) {
132
- var imgs = (root.querySelectorAll) ? root.querySelectorAll('img') : [];
133
- for (var i = 0; i < imgs.length; i++) {
134
- imgs[i].setAttribute('draggable', 'false');
135
- imgs[i].ondragstart = function() { return false; };
136
- imgs[i].oncontextmenu = function() { return false; };
137
  }
138
  }
139
- lockImages(doc);
140
-
141
- // Debounce to reduce excessive firing
142
- var lockTimer = null;
143
- var obs = new MutationObserver(function (mutations) {
144
- if (lockTimer) return; // skip if already scheduled
145
- lockTimer = setTimeout(function() {
146
- lockTimer = null;
147
- for (var m = 0; m < mutations.length; m++) {
148
- var nodes = mutations[m].addedNodes;
149
- for (var n = 0; n < nodes.length; n++) {
150
- if (nodes[n].nodeType === 1) lockImages(nodes[n]);
151
- }
152
- }
153
- }, 100); // debounce 100ms
154
- });
155
- obs.observe(doc.body, { childList: true, subtree: true });
156
-
157
- // ── Layer 8: Neuter Blob URL creation for images ────────────────────
158
- // Prevents programmatic extraction via createObjectURL
159
- var origCreateObjectURL = URL.createObjectURL;
160
- URL.createObjectURL = function(obj) {
161
- if (obj instanceof Blob && obj.type && obj.type.startsWith('image/')) {
162
- console.warn('[OphthalmoCapture] Blob URL creation blocked for images');
163
- return '';
164
- }
165
- return origCreateObjectURL.call(URL, obj);
166
- };
167
-
168
- })();
169
- </script>
170
  """
171
 
172
 
173
  def inject_image_protection():
174
- """Inject all CSS + JS image-protection layers into the page.
175
 
176
- Call this ONCE near the top of main.py, after st.set_page_config().
177
- CSS is re-injected every rerun (Streamlit requires it in the render tree).
178
- JS has its own internal guard to avoid duplicate listeners.
 
179
  """
180
- # CSS — MUST be re-injected every rerun so it stays in the DOM
181
- st.markdown(_PROTECTION_CSS, unsafe_allow_html=True)
182
-
183
- # JS — uses components.html; internal guard prevents duplicate listeners.
184
- # height=0, width=0 makes the iframe invisible and avoids layout shifts.
185
- components.html(_PROTECTION_JS_HTML, height=0, width=0, scrolling=False)
 
3
  Injects CSS and JavaScript into the Streamlit page to prevent users from
4
  downloading, dragging, or otherwise saving the confidential medical images.
5
 
6
+ APPROACH:
7
  Streamlit's st.markdown(unsafe_allow_html=True) renders <style> tags but
8
+ STRIPS <script> tags. To run JS without components.html (which creates
9
+ iframes that cause layout shifts on HF Spaces), we use the classic
10
+ <img src=x onerror="…"> trick the onerror handler executes inline JS
11
+ directly in the main Streamlit DOM, with no iframe.
 
12
 
13
  Protection layers (defence-in-depth):
14
+ 1. CSS: pointer-events:none, user-select:none on <img>.
15
+ 2. CSS: transparent ::after overlay on stImage containers.
16
+ 3. CSS: -webkit-touch-callout:none for mobile.
17
+ 4. JS: contextmenu blocked on entire document.
18
+ 5. JS: Ctrl+S / Ctrl+U / Ctrl+P / F12 / DevTools shortcuts blocked.
19
+ 6. JS: dragstart blocked for images.
20
+ 7. JS: MutationObserver re-applies draggable=false to new images.
 
 
 
 
 
 
 
 
 
 
 
 
21
  """
22
 
23
  import streamlit as st
 
24
 
25
+ # ── CSS + JS injected via st.markdown ────────────────────────────────────────
26
+ _PROTECTION_HTML = """
27
  <style>
28
  /* Layer 1: Disable ALL interaction on <img> tags */
29
+ img:not([data-protection-trigger]) {
30
  pointer-events: none !important;
31
  user-select: none !important;
32
  -webkit-user-select: none !important;
 
55
  -webkit-user-drag: none !important;
56
  user-drag: none !important;
57
  }
 
 
58
 
59
+ /* Hide the trigger pixel completely */
60
+ img[data-protection-trigger] {
61
+ display: none !important;
62
+ }
63
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ <!-- JS via onerror trick runs directly in Streamlit DOM, no iframe -->
66
+ <img data-protection-trigger src="x" onerror="
67
+ (function(doc){
68
+ if(doc.__ophthalmo_protection__) return;
69
+ doc.__ophthalmo_protection__=true;
70
+
71
+ function block(e){e.preventDefault();e.stopPropagation();e.stopImmediatePropagation();return false;}
72
+
73
+ doc.addEventListener('contextmenu',function(e){return block(e);},true);
74
+
75
+ doc.addEventListener('keydown',function(e){
76
+ var dominated=false;
77
+ var ctrl=e.ctrlKey||e.metaKey;
78
+ var key=e.key?e.key.toLowerCase():'';
79
+ if(ctrl&&key==='s')dominated=true;
80
+ if(ctrl&&key==='u')dominated=true;
81
+ if(ctrl&&key==='p')dominated=true;
82
+ if(e.keyCode===123)dominated=true;
83
+ if(ctrl&&e.shiftKey&&key==='i')dominated=true;
84
+ if(ctrl&&e.shiftKey&&key==='j')dominated=true;
85
+ if(ctrl&&e.shiftKey&&key==='c')dominated=true;
86
+ if(dominated)return block(e);
87
+ },true);
88
+
89
+ doc.addEventListener('dragstart',function(e){
90
+ if(e.target&&e.target.tagName==='IMG')return block(e);
91
+ },true);
92
+
93
+ function lockImgs(root){
94
+ var imgs=root.querySelectorAll?root.querySelectorAll('img:not([data-protection-trigger])'):[];
95
+ for(var i=0;i<imgs.length;i++){
96
+ imgs[i].setAttribute('draggable','false');
97
+ imgs[i].ondragstart=function(){return false;};
98
+ imgs[i].oncontextmenu=function(){return false;};
 
 
 
 
 
 
 
99
  }
100
  }
101
+ lockImgs(doc);
102
+
103
+ var t=null;
104
+ new MutationObserver(function(muts){
105
+ if(t)return;
106
+ t=setTimeout(function(){
107
+ t=null;
108
+ lockImgs(doc);
109
+ },200);
110
+ }).observe(doc.body,{childList:true,subtree:true});
111
+
112
+ })(document);
113
+ " />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  """
115
 
116
 
117
  def inject_image_protection():
118
+ """Inject CSS + JS image-protection layers into the page.
119
 
120
+ Uses st.markdown only NO components.html iframes so there are
121
+ zero layout shifts. The JS guard (doc.__ophthalmo_protection__)
122
+ ensures listeners are attached only once per page lifecycle even
123
+ though st.markdown re-renders on every Streamlit rerun.
124
  """
125
+ st.markdown(_PROTECTION_HTML, unsafe_allow_html=True)
 
 
 
 
 
interface/main.py CHANGED
@@ -250,23 +250,19 @@ with st.spinner(t("loading_whisper", model=selected_model)):
250
  model = load_whisper_model(selected_model)
251
  # ── BROWSER CLOSE GUARD (beforeunload) ───────────────────────────────────
252
  # Warn the user when they try to close/reload the tab with data in session.
 
253
  if sm.has_undownloaded_data() and not st.session_state.get("session_downloaded", False):
254
- st.components.v1.html(
255
- """
256
- <script>
257
  (function(){
258
- try {
259
- var d = window.parent || window;
260
- d.addEventListener('beforeunload', function (e) {
261
- e.preventDefault();
262
- e.returnValue = '';
263
- });
264
- } catch(err) {}
265
  })();
266
- </script>
267
- """,
268
- height=0,
269
- width=0,
270
  )
271
  # ── MAIN CONTENT ─────────────────────────────────────────────────────────────
272
  st.title(f"{config.APP_ICON} {config.APP_TITLE}")
 
250
  model = load_whisper_model(selected_model)
251
  # ── BROWSER CLOSE GUARD (beforeunload) ───────────────────────────────────
252
  # Warn the user when they try to close/reload the tab with data in session.
253
+ # Uses img onerror trick to avoid creating an iframe (which causes layout shifts).
254
  if sm.has_undownloaded_data() and not st.session_state.get("session_downloaded", False):
255
+ st.markdown(
256
+ """<img src="x" style="display:none!important" onerror="
 
257
  (function(){
258
+ if(window.__beforeunload_set__)return;
259
+ window.__beforeunload_set__=true;
260
+ window.addEventListener('beforeunload',function(e){
261
+ e.preventDefault();e.returnValue='';
262
+ });
 
 
263
  })();
264
+ " />""",
265
+ unsafe_allow_html=True,
 
 
266
  )
267
  # ── MAIN CONTENT ─────────────────────────────────────────────────────────────
268
  st.title(f"{config.APP_ICON} {config.APP_TITLE}")