Merge pull request #60 from outis1one/claude/audit-silent-send-5thF8
Claude/audit silent send 5th f8
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Silent Send",
|
"name": "Silent Send",
|
||||||
"version": "0.9.21",
|
"version": "0.9.23",
|
||||||
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
|
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["src/content/content.js", "src/content/early-hook.js"],
|
"resources": ["src/content/content.js", "src/content/early-hook.js", "src/lib/document-scanner.js"],
|
||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Silent Send",
|
"name": "Silent Send",
|
||||||
"version": "0.9.21",
|
"version": "0.9.23",
|
||||||
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
|
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["src/content/content.js", "src/content/early-hook.js"],
|
"resources": ["src/content/content.js", "src/content/early-hook.js", "src/lib/document-scanner.js"],
|
||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "silent-send",
|
"name": "silent-send",
|
||||||
"version": "0.9.21",
|
"version": "0.9.23",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Browser extension that substitutes personal data before sending to AI services",
|
"description": "Browser extension that substitutes personal data before sending to AI services",
|
||||||
|
|||||||
+114
-26
@@ -737,6 +737,52 @@
|
|||||||
return isConfigured();
|
return isConfigured();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Document Scanner — process file uploads in FormData
|
||||||
|
// ============================================================
|
||||||
|
async function scanFormData(formData) {
|
||||||
|
const newForm = new FormData();
|
||||||
|
const allReplacements = [];
|
||||||
|
let anyModified = false;
|
||||||
|
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
if (value instanceof File || value instanceof Blob) {
|
||||||
|
const filename = value instanceof File ? value.name : (key || 'file');
|
||||||
|
try {
|
||||||
|
const result = await globalThis.DocumentScanner.processUpload(
|
||||||
|
value, filename, substituteAll
|
||||||
|
);
|
||||||
|
if (result.replacements.length > 0) {
|
||||||
|
anyModified = true;
|
||||||
|
allReplacements.push(...result.replacements);
|
||||||
|
const newFile = new File([result.file], result.filename, { type: result.file.type });
|
||||||
|
newForm.append(key, newFile, result.filename);
|
||||||
|
} else {
|
||||||
|
newForm.append(key, value, filename);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Silent Send] Document scan failed for', filename, e);
|
||||||
|
newForm.append(key, value, filename);
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'string' && value.length >= MIN_STRING_LENGTH) {
|
||||||
|
// Also substitute string fields in the FormData
|
||||||
|
const result = substituteAll(value);
|
||||||
|
if (result.modified) {
|
||||||
|
anyModified = true;
|
||||||
|
allReplacements.push(...result.replacements);
|
||||||
|
newForm.append(key, result.text);
|
||||||
|
} else {
|
||||||
|
newForm.append(key, value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newForm.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyModified) return null;
|
||||||
|
return { formData: newForm, replacements: allReplacements };
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Fetch Interception — scans ALL POST requests with a body.
|
// Fetch Interception — scans ALL POST requests with a body.
|
||||||
// Service-agnostic: doesn't depend on URL patterns.
|
// Service-agnostic: doesn't depend on URL patterns.
|
||||||
@@ -765,35 +811,55 @@
|
|||||||
const urlStr = typeof url === 'string' ? url : url?.url || '';
|
const urlStr = typeof url === 'string' ? url : url?.url || '';
|
||||||
const method = (options?.method || 'GET').toUpperCase();
|
const method = (options?.method || 'GET').toUpperCase();
|
||||||
|
|
||||||
// Only intercept POST/PUT/PATCH with a string body
|
// Only intercept POST/PUT/PATCH
|
||||||
if (
|
if (
|
||||||
(method === 'POST' || method === 'PUT' || method === 'PATCH') &&
|
(method === 'POST' || method === 'PUT' || method === 'PATCH') &&
|
||||||
options?.body && typeof options.body === 'string' &&
|
options?.body && !shouldSkipUrl(urlStr)
|
||||||
!shouldSkipUrl(urlStr)
|
|
||||||
) {
|
) {
|
||||||
try {
|
// FormData body — scan file uploads via DocumentScanner
|
||||||
// Try JSON
|
if (options.body instanceof FormData && typeof globalThis.DocumentScanner !== 'undefined') {
|
||||||
const body = JSON.parse(options.body);
|
try {
|
||||||
const { modified, replacements } = processBody(body);
|
const scannedForm = await scanFormData(options.body);
|
||||||
|
if (scannedForm) {
|
||||||
if (modified) {
|
options = { ...options, body: scannedForm.formData };
|
||||||
options = { ...options, body: JSON.stringify(body) };
|
if (scannedForm.replacements.length > 0) {
|
||||||
notifySubstitutions(replacements);
|
notifySubstitutions(scannedForm.replacements);
|
||||||
console.log(
|
console.log(
|
||||||
`[Silent Send] Substituted ${replacements.length} value(s) in ${urlStr}`
|
`[Silent Send] Substituted ${scannedForm.replacements.length} value(s) in file upload to ${urlStr}`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Silent Send] FormData scan failed:', e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
}
|
||||||
// Not JSON — try raw string substitution (form data, etc.)
|
|
||||||
if (options.body.length > MIN_STRING_LENGTH) {
|
// String body — JSON or raw text
|
||||||
const result = substituteAll(options.body);
|
if (typeof options.body === 'string') {
|
||||||
if (result.modified) {
|
try {
|
||||||
options = { ...options, body: result.text };
|
// Try JSON
|
||||||
notifySubstitutions(result.replacements);
|
const body = JSON.parse(options.body);
|
||||||
|
const { modified, replacements } = processBody(body);
|
||||||
|
|
||||||
|
if (modified) {
|
||||||
|
options = { ...options, body: JSON.stringify(body) };
|
||||||
|
notifySubstitutions(replacements);
|
||||||
console.log(
|
console.log(
|
||||||
`[Silent Send] Substituted ${result.replacements.length} value(s) in form body`
|
`[Silent Send] Substituted ${replacements.length} value(s) in ${urlStr}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON — try raw string substitution (form data, etc.)
|
||||||
|
if (options.body.length > MIN_STRING_LENGTH) {
|
||||||
|
const result = substituteAll(options.body);
|
||||||
|
if (result.modified) {
|
||||||
|
options = { ...options, body: result.text };
|
||||||
|
notifySubstitutions(result.replacements);
|
||||||
|
console.log(
|
||||||
|
`[Silent Send] Substituted ${result.replacements.length} value(s) in form body`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,15 +1035,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unrevealText(text) {
|
||||||
|
const pairs = getRevealPairs();
|
||||||
|
let result = text;
|
||||||
|
for (const p of pairs) {
|
||||||
|
const escaped = esc(p.to); // p.to is the real value
|
||||||
|
const regex = new RegExp(escaped, p.caseSensitive ? 'g' : 'gi');
|
||||||
|
result = result.replace(regex, p.from); // p.from is the substitute
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function unrevealInElement(el) {
|
function unrevealInElement(el) {
|
||||||
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
if (SKIP_REVEAL_TAGS.has(el.tagName)) return;
|
||||||
|
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
||||||
|
acceptNode(node) {
|
||||||
|
const parent = node.parentElement;
|
||||||
|
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||||
|
if (parent?.closest?.('.ss-autodetect-warning, .ss-presend-warning, .ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
|
}
|
||||||
|
});
|
||||||
let textNode;
|
let textNode;
|
||||||
while ((textNode = walker.nextNode())) {
|
while ((textNode = walker.nextNode())) {
|
||||||
const original = originalTexts.get(textNode);
|
const text = textNode.textContent;
|
||||||
if (original && textNode.textContent !== original) {
|
if (!text || text.length < MIN_STRING_LENGTH) continue;
|
||||||
textNode.textContent = original;
|
const unrevealed = unrevealText(text);
|
||||||
|
if (unrevealed !== text) {
|
||||||
|
textNode.textContent = unrevealed;
|
||||||
|
// Update saved original so future reveals start from the right state
|
||||||
|
originalTexts.set(textNode, unrevealed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,14 @@
|
|||||||
// Merge active profiles into a flat identity object for the content script
|
// Merge active profiles into a flat identity object for the content script
|
||||||
const identity = mergeProfiles(identityData);
|
const identity = mergeProfiles(identityData);
|
||||||
|
|
||||||
|
// Inject the document scanner into the page's world first (sets globalThis.DocumentScanner)
|
||||||
|
const docScannerScript = document.createElement('script');
|
||||||
|
docScannerScript.type = 'module';
|
||||||
|
docScannerScript.src = api.runtime.getURL('src/lib/document-scanner.js');
|
||||||
|
(document.head || document.documentElement).appendChild(docScannerScript);
|
||||||
|
await new Promise(resolve => { docScannerScript.onload = resolve; docScannerScript.onerror = resolve; });
|
||||||
|
docScannerScript.remove();
|
||||||
|
|
||||||
// Inject the main interception script into the page's world
|
// Inject the main interception script into the page's world
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
|
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
|
||||||
|
|||||||
Reference in New Issue
Block a user