diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..bfabef8 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-05-18 - Cross-Site Scripting (XSS) via injectJavaScript in WebView +**Vulnerability:** The application was vulnerable to Cross-Site Scripting (XSS) due to the use of `injectJavaScript` to pass data (base64 JSON strings) directly into the WebView execution context in `src/screens/HomeScreen.tsx` and `src/screens/ShiftScreen.tsx`. +**Learning:** Using `injectJavaScript` with string interpolation can allow malicious scripts to be executed within the WebView context, especially when handling arbitrary data like images or large strings. +**Prevention:** Avoid using `injectJavaScript` for passing data. Instead, establish a secure communication channel using `postMessage`. The WebView should listen for messages via `window.document.addEventListener('message', ...)` and `window.addEventListener('message', ...)`, and send a 'READY' message to the React Native app when it's initialized to prevent race conditions. diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 7779ce3..d10f49b 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -48,6 +48,23 @@ window.runTesseract = async function(base64JsonStr) { window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: e.message || e.toString() })); } }; + +const handleMsg = function(e) { + try { + const data = JSON.parse(e.data); + if(data.type === "OCR_REQUEST") { + if(window.runTesseract) { + window.runTesseract(data.payload); + } else { + window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: "Motore OCR non pronto." })); + } + } + } catch(err) {} +}; +window.document.addEventListener('message', handleMsg); +window.addEventListener('message', handleMsg); + +window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'READY' })); `; function PinnedFlightCardComponent({ item, colors }: { item: any; colors: any }) { @@ -166,6 +183,7 @@ export default function HomeScreen({ isFocused }: { isFocused?: boolean }) { const [newEndM, setNewEndM] = useState('00'); const [uploadMode, setUploadMode] = useState<'image' | 'manual' | null>(null); const [pinnedFlight, setPinnedFlight] = useState(null); + const [isEngineReady, setIsEngineReady] = useState(false); const webViewRef = useRef(null); @@ -283,15 +301,12 @@ export default function HomeScreen({ isFocused }: { isFocused?: boolean }) { setProcessing(true); setOcrText(''); const base64List = result.assets.map(a => `data:image/jpeg;base64,${a.base64}`); const base64Json = JSON.stringify(base64List); - // Use postMessage pattern to avoid script-injection risks with injectJavaScript - webViewRef.current?.injectJavaScript(` - if(window.runTesseract){ - window.runTesseract(${JSON.stringify(base64Json)}); - } else { - window.ReactNativeWebView.postMessage(JSON.stringify({success:false,error:'OCR non pronto'})); - } - true; - `); + if (!isEngineReady) { + Alert.alert("Errore", "OCR non pronto. Attendi qualche istante."); + setProcessing(false); + return; + } + webViewRef.current?.postMessage(JSON.stringify({ type: 'OCR_REQUEST', payload: base64Json })); } } catch (e) { if (__DEV__) console.error('[imagePicker]', e); setProcessing(false); } }; @@ -299,6 +314,7 @@ export default function HomeScreen({ isFocused }: { isFocused?: boolean }) { const handleWebViewMessage = (event: any) => { try { const r = JSON.parse(event.nativeEvent.data); + if (r.type === 'READY') { setIsEngineReady(true); return; } if (r.success) setOcrText(r.text); else Alert.alert('Errore riconoscimento testo', r.error || 'Prova con un\'immagine più nitida o meglio illuminata.'); } catch (e) { if (__DEV__) console.error('[ocrMessage]', e); } finally { setProcessing(false); } diff --git a/src/screens/ShiftScreen.tsx b/src/screens/ShiftScreen.tsx index 128e708..991a0f2 100644 --- a/src/screens/ShiftScreen.tsx +++ b/src/screens/ShiftScreen.tsx @@ -16,6 +16,7 @@ export default function ShiftScreen() { const [ocrText, setOcrText] = useState(''); const [processing, setProcessing] = useState(false); const webViewRef = useRef(null); + const [isEngineReady, setIsEngineReady] = useState(false); const pickImage = async () => { try { @@ -34,15 +35,12 @@ export default function ShiftScreen() { const base64List = result.assets.map(a => `data:image/jpeg;base64,${a.base64}`); const base64Json = JSON.stringify(base64List).replace(/'/g, "\\'"); - const jsCode = ` - if (window.runTesseract) { - window.runTesseract('${base64Json}'); - } else { - window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: "Motore OCR non pronto." })); - } - true; - `; - webViewRef.current?.injectJavaScript(jsCode); + if (!isEngineReady) { + Alert.alert("Errore", "Motore OCR non pronto. Riprova."); + setProcessing(false); + return; + } + webViewRef.current?.postMessage(JSON.stringify({ type: 'OCR_REQUEST', payload: base64Json })); } } catch (e) { Alert.alert("Errore OCR", "Impossibile elaborare l'immagine."); @@ -54,6 +52,10 @@ export default function ShiftScreen() { const rawData = event.nativeEvent.data; try { const result = JSON.parse(rawData); + if (result.type === 'READY') { + setIsEngineReady(true); + return; + } if (result.success) { setOcrText(result.text); } else { @@ -230,6 +232,23 @@ export default function ShiftScreen() { window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: e.message || e.toString() })); } }; + + const handleMsg = function(e) { + try { + const data = JSON.parse(e.data); + if(data.type === "OCR_REQUEST") { + if(window.runTesseract) { + window.runTesseract(data.payload); + } else { + window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: "Motore OCR non pronto." })); + } + } + } catch(err) {} + }; + window.document.addEventListener('message', handleMsg); + window.addEventListener('message', handleMsg); + + window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'READY' }));