Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## 2026-04-27 - XSS in React Native WebView

**Vulnerability:** React Native WebViews `injectJavaScript` was using unsafe string interpolation with base64 images that could lead to XSS attacks or syntax errors if the string contained single or double quotes unescaped correctly.

**Learning:** Passing user-supplied strings directly into `injectJavaScript` using template interpolation exposes a risk for Script Injection/XSS in WebViews.

**Prevention:** To avoid this, prefer establishing a two-way communication channel using `postMessage` (from WebView to React Native) and `webViewRef.current?.postMessage` (from React Native to WebView). Ensure the HTML snippet adds `message` listeners for both `window` and `document`, and uses a `READY` message handshake to confirm the engine is ready.
52 changes: 35 additions & 17 deletions src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,41 @@ const weatherMap: Record<number, { text: string; icon: string }> = {
const engineHtml = `<!DOCTYPE html><html lang="it"><head>
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script></head>
<body style="background-color:transparent;"><script>
window.runTesseract = async function(base64JsonStr) {
// Prevent XSS by using postMessage instead of string interpolation
window.addEventListener('message', async function(event) {
try {
const images = JSON.parse(base64JsonStr);
let combinedText = '';
for (let i = 0; i < images.length; i++) {
const ret = await Tesseract.recognize(images[i], 'ita+eng');
combinedText += ret.data.text + '\\n\\n';
const data = JSON.parse(event.data);
if (data.type === 'RUN_OCR') {
const images = data.images;
let combinedText = '';
for (let i = 0; i < images.length; i++) {
const ret = await Tesseract.recognize(images[i], 'ita+eng');
combinedText += ret.data.text + '\\n\\n';
}
window.ReactNativeWebView.postMessage(JSON.stringify({ success: true, text: combinedText }));
}
window.ReactNativeWebView.postMessage(JSON.stringify({ success: true, text: combinedText }));
} catch (e) {
window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: e.message || e.toString() }));
}
};
});
document.addEventListener('message', async function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'RUN_OCR') {
const images = data.images;
let combinedText = '';
for (let i = 0; i < images.length; i++) {
const ret = await Tesseract.recognize(images[i], 'ita+eng');
combinedText += ret.data.text + '\\n\\n';
}
window.ReactNativeWebView.postMessage(JSON.stringify({ success: true, text: combinedText }));
}
} catch (e) {
window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: e.message || e.toString() }));
}
});
// Handshake to notify React Native that WebView is ready
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'READY' }));
</script></body></html>`;

function PinnedFlightCardComponent({ item, colors }: { item: any; colors: any }) {
Expand Down Expand Up @@ -289,23 +311,19 @@ export default function HomeScreen({ isFocused }: { isFocused?: boolean }) {
setImageList(result.assets.map(a => a.uri));
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;
`);
webViewRef.current?.postMessage(JSON.stringify({ type: 'RUN_OCR', images: base64List }));
}
} catch (e) { if (__DEV__) console.error('[imagePicker]', e); setProcessing(false); }
};

const handleWebViewMessage = (event: any) => {
try {
const r = JSON.parse(event.nativeEvent.data);
if (r.type === 'READY') {
// Engine is ready, can proceed with messages if needed
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); }
Expand Down
54 changes: 35 additions & 19 deletions src/screens/ShiftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,7 @@ export default function ShiftScreen() {
setOcrText('');

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);
webViewRef.current?.postMessage(JSON.stringify({ type: 'RUN_OCR', images: base64List }));
}
} catch (e) {
Alert.alert("Errore OCR", "Impossibile elaborare l'immagine.");
Expand All @@ -53,6 +43,10 @@ export default function ShiftScreen() {
const rawData = event.nativeEvent.data;
try {
const result = JSON.parse(rawData);
if (result.type === 'READY') {
// Engine is ready
return;
}
if (result.success) {
setOcrText(result.text);
} else {
Expand Down Expand Up @@ -216,19 +210,41 @@ export default function ShiftScreen() {
</head>
<body style="background-color: transparent;">
<script>
window.runTesseract = async function(base64JsonStr) {
// Prevent XSS by using postMessage instead of string interpolation
window.addEventListener('message', async function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'RUN_OCR') {
const images = data.images;
let combinedText = '';
for (let i = 0; i < images.length; i++) {
const ret = await Tesseract.recognize(images[i], 'ita+eng');
combinedText += ret.data.text + '\\n\\n';
}
window.ReactNativeWebView.postMessage(JSON.stringify({ success: true, text: combinedText }));
}
} catch (e) {
window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: e.message || e.toString() }));
}
});
document.addEventListener('message', async function(event) {
try {
const images = JSON.parse(base64JsonStr);
let combinedText = '';
for (let i = 0; i < images.length; i++) {
const ret = await Tesseract.recognize(images[i], 'ita+eng');
combinedText += ret.data.text + '\\n\\n';
const data = JSON.parse(event.data);
if (data.type === 'RUN_OCR') {
const images = data.images;
let combinedText = '';
for (let i = 0; i < images.length; i++) {
const ret = await Tesseract.recognize(images[i], 'ita+eng');
combinedText += ret.data.text + '\\n\\n';
}
window.ReactNativeWebView.postMessage(JSON.stringify({ success: true, text: combinedText }));
}
window.ReactNativeWebView.postMessage(JSON.stringify({ success: true, text: combinedText }));
} catch (e) {
window.ReactNativeWebView.postMessage(JSON.stringify({ success: false, error: e.message || e.toString() }));
}
};
});
// Handshake to notify React Native that WebView is ready
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'READY' }));
</script>
</body>
</html>
Expand Down
Loading