-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
447 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
const config = { | ||
suspiciousURLPatterns: [ | ||
/data:application\/octet-stream/i, | ||
/blob:/i, | ||
/javascript:/i | ||
], | ||
suspiciousHeaders: ['content-disposition', 'content-type'], | ||
logRetentionDays: 10, | ||
cacheDurationMs: 5 * 60 * 1000, | ||
}; | ||
|
||
const urlCache = new Map(); | ||
|
||
function memoize(fn, resolver) { | ||
const cache = new Map(); | ||
return (...args) => { | ||
const key = resolver ? resolver(...args) : args[0]; | ||
if (cache.has(key)) return cache.get(key); | ||
const result = fn(...args); | ||
cache.set(key, result); | ||
return result; | ||
}; | ||
} | ||
|
||
const checkSuspiciousURL = memoize((url) => { | ||
return config.suspiciousURLPatterns.some(pattern => pattern.test(url)); | ||
}, (url) => url); | ||
|
||
function debounce(func, delay) { | ||
let timeoutId; | ||
return (...args) => { | ||
clearTimeout(timeoutId); | ||
timeoutId = setTimeout(() => func(...args), delay); | ||
}; | ||
} | ||
|
||
const debouncedLogBlockedContent = debounce(logBlockedContent, 1000); | ||
|
||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { | ||
console.log('Received message:', request); | ||
switch (request.action) { | ||
case "logWarning": | ||
debouncedLogBlockedContent(sender.tab.url, request.patterns, Date.now()); | ||
console.warn(request.message); | ||
break; | ||
case "analyzeURL": | ||
const cachedResult = urlCache.get(request.url); | ||
if (cachedResult && (Date.now() - cachedResult.timestamp < config.cacheDurationMs)) { | ||
sendResponse({isSuspicious: cachedResult.isSuspicious}); | ||
} else { | ||
const isSuspicious = checkSuspiciousURL(request.url); | ||
urlCache.set(request.url, {isSuspicious, timestamp: Date.now()}); | ||
sendResponse({isSuspicious}); | ||
} | ||
return true; | ||
case "exportLogs": | ||
chrome.storage.local.get(['blockedLogs'], result => { | ||
sendResponse({ logs: result.blockedLogs || [] }); | ||
}); | ||
return true; | ||
case "updateConfig": | ||
updateConfig(request.newConfig); | ||
sendResponse({success: true}); | ||
return true; | ||
} | ||
}); | ||
|
||
function logBlockedContent(url, patterns, timestamp) { | ||
chrome.storage.local.get(['blockedLogs'], function(result) { | ||
let logs = result.blockedLogs || []; | ||
logs.push({ url, patterns, timestamp }); | ||
const retentionDate = Date.now() - (config.logRetentionDays * 24 * 60 * 60 * 1000); | ||
logs = logs.filter(log => log.timestamp > retentionDate); | ||
|
||
chrome.storage.local.set({ blockedLogs: logs }, () => { | ||
if (chrome.runtime.lastError) { | ||
console.error('Error saving logs:', chrome.runtime.lastError); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
function updateConfig(newConfig) { | ||
Object.assign(config, newConfig); | ||
urlCache.clear(); | ||
} | ||
|
||
const checkSuspiciousHeaders = memoize((headers) => { | ||
return headers.some(header => | ||
config.suspiciousHeaders.includes(header.name.toLowerCase()) && | ||
/attachment|application\/octet-stream/i.test(header.value) | ||
); | ||
}, (headers) => JSON.stringify(headers)); | ||
|
||
chrome.webRequest.onHeadersReceived.addListener( | ||
(details) => { | ||
const hasSuspiciousHeaders = checkSuspiciousHeaders(details.responseHeaders); | ||
|
||
if (hasSuspiciousHeaders) { | ||
chrome.tabs.sendMessage(details.tabId, {action: "suspiciousHeadersDetected"}) | ||
.catch(error => console.error('Error sending message:', error)); | ||
} | ||
|
||
return {responseHeaders: details.responseHeaders}; | ||
}, | ||
{urls: ["<all_urls>"]}, | ||
["responseHeaders"] | ||
); | ||
|
||
setInterval(() => { | ||
const now = Date.now(); | ||
urlCache.forEach((data, url) => { | ||
if (now - data.timestamp > config.cacheDurationMs) { | ||
urlCache.delete(url); | ||
} | ||
}); | ||
}, config.cacheDurationMs); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
class HTMLSmugglingBlocker { | ||
constructor() { | ||
this.blocked = false; this.suspiciousPatterns = [ | ||
{ pattern: /atob\s*\([^)]+\).*new\s+uint8array/is, weight: 3 }, | ||
{ pattern: /atob\s*\(\s*['"]([A-Za-z0-9+/=]{100,})['"].*\)/i, weight: 3 }, | ||
{ pattern: /new\s+blob\s*\(\s*\[\s*(?:data|atob\s*\()/i, weight: 3 }, | ||
{ pattern: /url\.createobjecturl\s*\(\s*(?:my)?blob\s*\)/i, weight: 3 }, | ||
{ pattern: /location(?:\s*\[\s*["']href["']\s*\])?\s*=\s*url/i, weight: 3 }, | ||
{ pattern: /url\.revokeobjecturl\s*\(\s*url\s*\)/i, weight: 2 }, | ||
{ pattern: /window\s*\[\s*(?:["']\w+["']\s*\+\s*)+["']\w+["']\s*\]/i, weight: 3 }, | ||
{ pattern: /document\s*\[\s*(?:["']\w+["']\s*\+\s*)+["']\w+["']\s*\]\s*\(\s*window\s*\[\s*(?:['"]at['"].*['"]o['"].*['"]b['"]\s*\]|\s*(?:["']\w+["']\s*\+\s*)+["']\w+["']\s*\])\s*\(['"][A-Za-z0-9+/=]+['"]\)\s*\)/i, weight: 4 }, | ||
{ pattern: /var\s+\w+=\w+;?\s*\(function\(\w+,\w+\)\{.*while\(!!\[\]\)\{try\{.*parseint.*\}catch\(\w+\)\{.*\}\}\}\(.*\)\);?/is, weight: 4 }, | ||
{ pattern: /blob\s*\(\s*\[[^\]]+\]\s*,\s*\{\s*type\s*:\s*['"](?:application\/octet-stream|text\/html|octet\/stream)['"](?:\s*,\s*encoding\s*:\s*['"]base64['"])?\s*\}\s*\)/is, weight: 3 }, | ||
{ pattern: /\.style\s*=\s*['"]display:\s*none['"].*\.href\s*=.*\.download\s*=/is, weight: 3 }, | ||
{ pattern: /\.click\s*\(\s*\).*url\.revokeobjecturl/is, weight: 3 }, | ||
{ pattern: /href\s*=\s*["']data:(?:application\/octet-stream|image\/svg\+xml);base64,/i, weight: 3 }, | ||
{ pattern: /webassembly\s*\.\s*(?:instantiate(?:streaming)?|instance)/i, weight: 3 }, | ||
{ pattern: /navigator\.serviceworker\.register/i, weight: 2 }, | ||
{ pattern: /srcdoc\s*=\s*["'][^"']*<script/i, weight: 3 }, | ||
{ pattern: /function\s+(?:b64toarray|xor|base64toarraybuffer)\s*\([^)]*\)\s*{[\s\S]*?return\s+(?:bytes\.buffer|result);?}/i, weight: 3 }, | ||
{ pattern: /document\.createelement\(['"']embed['"']\)/i, weight: 3 }, | ||
{ pattern: /\.setattribute\(['"']src['"']\s*,\s*.*\)/i, weight: 2 }, | ||
{ pattern: /window\.navigator\.mssaveoropenblob\s*\(\s*blob\s*,\s*filename\s*\)/i, weight: 3 }, | ||
{ pattern: /(?:window\.)?url\.createobjecturl\s*\(\s*(?:blob|[^)]+)\s*\)/i, weight: 2 }, | ||
{ pattern: /(?:a|element)\.download\s*=\s*(?:filename|['"][^'"]+['"])/i, weight: 2 }, | ||
{ pattern: /string\.fromcharcode\(.*\)/i, weight: 2 }, | ||
{ pattern: /\.charcodeat\(.*\)/i, weight: 2 }, | ||
{ pattern: /document\.getelementbyid\(['"']passwordid['"']\)\.value/i, weight: 3 }, | ||
{ pattern: /import\s*\(\s*url\.createobjecturl\s*\(/i, weight: 3 }, | ||
{ pattern: /\w+\s*\(\s*\w+\s*\(\s*['"][A-Za-z0-9+/=]{50,}['"]\s*\)\s*\)/i, weight: 3 }, | ||
{ pattern: /(?:window\.)?atob\s*\(/i, weight: 2 }, | ||
{ pattern: /uint8[aA]rray\s*\(\s*(?:(?!len)[^)])*\)/i, weight: 2 }, | ||
{ pattern: /mssaveoropenblob|mssaveblob/i, weight: 3 }, | ||
{ pattern: /base64toarraybuffer/i, weight: 3 }, | ||
{ pattern: /wasm[_-]?exec\.js/i, weight: 2 }, | ||
{ pattern: /\.wasm/i, weight: 3 }, | ||
{ pattern: /new\s+go\s*\(\s*\)/i, weight: 3 }, | ||
{ pattern: /go\s*\.\s*run\s*\(/i, weight: 3 }, | ||
{ pattern: /<embed[^>]*base64/i, weight: 3 }, | ||
{ pattern: /xmlhttprequest\(\).*\.responsetype\s*=\s*['"]arraybuffer['"]/i, weight: 3 }, | ||
{ pattern: /new\s+dataview\(.*\).*\.getuint8\(.*\).*\.setuint8\(/i, weight: 3 }, | ||
{ pattern: /[^\w](\w+)\s*=\s*(\w+)\s*\^\s*(\w+)/i, weight: 2 }, | ||
{ pattern: /\.slice\(\s*\w+\s*-\s*\d+\s*,\s*\w+\s*-\s*\d+\s*\)/i, weight: 2 }, | ||
{ pattern: /for\s*\([^)]+\)\s*\{[^}]*string\.fromcharcode\([^)]+\)/i, weight: 3 }, | ||
]; | ||
this.threshold = 4; | ||
this.setupListeners(); | ||
} | ||
|
||
setupListeners() { | ||
this.analyzeContent(); | ||
|
||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { | ||
if (request.action === "analyzeContent") { | ||
this.analyzeContent(); | ||
} else if (request.action === "getBlockedStatus") { | ||
sendResponse({blocked: this.blocked}); | ||
} else if (request.action === "suspiciousHeadersDetected") { | ||
this.handleSuspiciousHeaders(); | ||
} | ||
}); | ||
|
||
this.setupObserver(); | ||
} | ||
|
||
setupObserver() { | ||
const observer = new MutationObserver(() => { | ||
this.analyzeContent(); | ||
}); | ||
|
||
observer.observe(document.documentElement, { | ||
childList: true, | ||
subtree: true | ||
}); | ||
} | ||
|
||
analyzeContent() { | ||
console.log("HTML Smuggling Blocker: Analyzing content"); | ||
const htmlContent = document.documentElement.outerHTML; | ||
|
||
let score = 0; | ||
const detectedPatterns = []; | ||
|
||
this.suspiciousPatterns.forEach(({pattern, weight}) => { | ||
if (pattern.test(htmlContent)) { | ||
score += weight; | ||
detectedPatterns.push(pattern.toString()); | ||
} | ||
}); | ||
|
||
if (score >= this.threshold) { | ||
console.log("HTML Smuggling Blocker: Suspicious content detected"); | ||
this.blocked = true; | ||
|
||
const elementsRemoved = this.removeSuspiciousElements(); | ||
const scriptsDisabled = this.disableInlineScripts(); | ||
const svgScriptsNeutralized = this.neutralizeSVGScripts(); | ||
const embedElementsRemoved = this.removeEmbedElements(); | ||
|
||
if (elementsRemoved > 0 || scriptsDisabled > 0 || svgScriptsNeutralized > 0 || embedElementsRemoved > 0) { | ||
this.logWarning(elementsRemoved, scriptsDisabled, svgScriptsNeutralized, embedElementsRemoved, detectedPatterns); | ||
} | ||
} else { | ||
console.log("HTML Smuggling Blocker: No suspicious content detected"); | ||
this.blocked = false; | ||
this.allowContent(); | ||
} | ||
} | ||
|
||
removeSuspiciousElements() { | ||
const suspiciousElements = document.querySelectorAll( | ||
'a[download][href^="data:"], a[download][href^="blob:"]' | ||
); | ||
console.log(`HTML Smuggling Blocker: Removed ${suspiciousElements.length} suspicious elements`); | ||
suspiciousElements.forEach(el => this.removeElement(el)); | ||
return suspiciousElements.length; | ||
} | ||
|
||
disableInlineScripts() { | ||
const inlineScripts = document.querySelectorAll('script:not([src])'); | ||
console.log(`HTML Smuggling Blocker: Analyzing ${inlineScripts.length} inline scripts`); | ||
let disabledCount = 0; | ||
inlineScripts.forEach(script => { | ||
if (this.isSuspiciousScript(script.textContent)) { | ||
this.removeElement(script); | ||
disabledCount++; | ||
} | ||
}); | ||
return disabledCount; | ||
} | ||
|
||
isSuspiciousScript(scriptContent) { | ||
return this.suspiciousPatterns.some(({pattern}) => pattern.test(scriptContent)); | ||
} | ||
|
||
neutralizeSVGScripts() { | ||
const svgScripts = document.querySelectorAll('svg script'); | ||
console.log(`HTML Smuggling Blocker: Neutralized ${svgScripts.length} SVG scripts`); | ||
svgScripts.forEach(el => this.removeElement(el)); | ||
return svgScripts.length; | ||
} | ||
|
||
removeEmbedElements() { | ||
const embedElements = document.querySelectorAll('embed'); | ||
console.log(`HTML Smuggling Blocker: Removed ${embedElements.length} embed elements`); | ||
embedElements.forEach(el => this.removeElement(el)); | ||
return embedElements.length; | ||
} | ||
|
||
logWarning(elementsRemoved, scriptsDisabled, svgScriptsNeutralized, embedElementsRemoved, detectedPatterns) { | ||
const message = `HTML Smuggling attempt blocked: ${elementsRemoved} elements removed, ${scriptsDisabled} scripts disabled, ${svgScriptsNeutralized} SVG scripts neutralized, ${embedElementsRemoved} embed elements removed. Detected patterns: ${detectedPatterns.join(', ')}`; | ||
console.warn(message); | ||
chrome.runtime.sendMessage({ | ||
action: "logWarning", | ||
message: message, | ||
patterns: detectedPatterns | ||
}); | ||
} | ||
|
||
removeElement(element) { | ||
if (element && element.parentNode) { | ||
element.parentNode.removeChild(element); | ||
} | ||
} | ||
|
||
allowContent() { | ||
document.documentElement.style.display = ''; | ||
console.log("HTML Smuggling Blocker: Content allowed"); | ||
} | ||
|
||
handleSuspiciousHeaders() { | ||
console.log("Suspicious headers detected"); | ||
this.showBlockedMessage(); | ||
} | ||
} | ||
|
||
new HTMLSmugglingBlocker(); |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"manifest_version": 3, | ||
"name": "SmuggleShield", | ||
"version": "2.0", | ||
"description": "Basic protection against HTML smuggling attempts.", | ||
"author": "Dhiraj Mishra (@RandomDhiraj)", | ||
"permissions": [ | ||
"webRequest", | ||
"storage", | ||
"tabs" | ||
], | ||
"host_permissions": [ | ||
"<all_urls>" | ||
], | ||
"background": { | ||
"service_worker": "background.js" | ||
}, | ||
"content_scripts": [ | ||
{ | ||
"matches": ["<all_urls>"], | ||
"js": ["content.js"], | ||
"run_at": "document_start" | ||
} | ||
], | ||
"action": { | ||
"default_popup": "popup.html", | ||
"default_icon": { | ||
"16": "icon/SmuggleShield.png", | ||
"48": "icon/SmuggleShield.png", | ||
"128": "icon/SmuggleShield.png" | ||
} | ||
}, | ||
"icons": { | ||
"16": "icon/SmuggleShield.png", | ||
"48": "icon/SmuggleShield.png", | ||
"128": "icon/SmuggleShield.png" | ||
}, | ||
"content_security_policy": { | ||
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'" | ||
} | ||
} |
Oops, something went wrong.