Moderate severityNVD Advisory· Published Aug 20, 2024· Updated Aug 21, 2024
Khoj Vulnerable to Stored Cross-site Scripting In Automate (Preview feature)
CVE-2024-43396
Description
Khoj is an application that creates personal AI agents. The Automation feature allows a user to insert arbitrary HTML inside the task instructions, resulting in a Stored XSS. The q parameter for the /api/automation endpoint does not get correctly sanitized when rendered on the page, resulting in the ability of users to inject arbitrary HTML/JS. This vulnerability is fixed in 1.15.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
khojPyPI | < 1.15.0 | 1.15.0 |
Affected products
1Patches
255be90cdd2f9Sanitize user input fields on Automations page of web client
2 files changed · +34 −24
src/khoj/interface/web/base_config.html+1 −0 modified@@ -14,6 +14,7 @@ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.13/css/intlTelInput.css"> </head> <script type="text/javascript" src="/static/assets/utils.js?v={{ khoj_version }}"></script> + <script type="text/javascript" src="/static/assets/purify.min.js?v={{ khoj_version }}"></script> <body class="khoj-configure"> <div class="khoj-header-wrapper"> <div class="filler"></div>
src/khoj/interface/web/config_automation.html+33 −24 modified@@ -278,21 +278,25 @@ <h2 class="section-title"> function updateAutomationRow(automation) { let automationId = automation.id; - let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`; + let automationSubject = DOMPurify.sanitize(automation.subject); + let automationSchedule = DOMPurify.sanitize(automation.schedule); + let automationQueryToRun = DOMPurify.sanitize(automation.query_to_run); + let automationCrontime = DOMPurify.sanitize(automation.crontime); + let automationNextRun = `Next run at ${automation.next}\nCron: ${automationCrontime}`; let scheduleEl = document.getElementById(`automation-schedule-${automationId}`); - scheduleEl.setAttribute('data-original', automation.schedule); - scheduleEl.setAttribute('data-cron', automation.crontime); + scheduleEl.setAttribute('data-original', automationSchedule); + scheduleEl.setAttribute('data-cron', automationCrontime); scheduleEl.setAttribute('title', automationNextRun); - scheduleEl.value = automation.schedule; + scheduleEl.value = automationSchedule; let subjectEl = document.getElementById(`automation-subject-${automationId}`); - subjectEl.setAttribute('data-original', automation.subject); - subjectEl.value = automation.subject; + subjectEl.setAttribute('data-original', automationSubject); + subjectEl.value = automationSubject; let queryEl = document.getElementById(`automation-queryToRun-${automationId}`); - queryEl.setAttribute('data-original', automation.query_to_run); - queryEl.value = automation.query_to_run; + queryEl.setAttribute('data-original', automationQueryToRun); + queryEl.value = automationQueryToRun; } function onClickEditAutomationCard(automationId) { @@ -387,7 +391,11 @@ <h2 class="section-title"> function generateAutomationRow(automation, isSuggested=false) { let automationId = automation.id; - let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`; + let automationSubject = DOMPurify.sanitize(automation.subject); + let automationSchedule = DOMPurify.sanitize(automation.schedule); + let automationQueryToRun = DOMPurify.sanitize(automation.query_to_run); + let automationCrontime = DOMPurify.sanitize(automation.crontime); + let automationNextRun = `Next run at ${automation.next}\nCron: ${automationCrontime}`; // Create card top elements let automationEl = document.createElement("div"); @@ -417,16 +425,16 @@ <h2 class="section-title"> subjectEl.id = `automation-subject-${automationId}`; subjectEl.classList.add(automationId, "fake-input"); subjectEl.name = "subject"; - subjectEl.setAttribute("data-original", automation.subject); - subjectEl.value = automation.subject; + subjectEl.setAttribute("data-original", automationSubject); + subjectEl.value = automationSubject; // automation share link let shareLinkEl = document.createElement("img"); shareLinkEl.id = `share-link-${automationId}`, shareLinkEl.className = "automation-share-icon"; shareLinkEl.src = "/static/assets/icons/share.svg"; shareLinkEl.alt = "Share"; - shareLinkEl.onclick = function(event) { copyShareLink(event, automationId, automation.subject, automation.crontime, automation.query_to_run); }; + shareLinkEl.onclick = function(event) { copyShareLink(event, automationId, automationSubject, automationCrontime, automationQueryToRun); }; // automation edit action let editIconEl = document.createElement("img"); @@ -441,18 +449,18 @@ <h2 class="section-title"> scheduleEl.id = `automation-schedule-${automationId}`; scheduleEl.name = "schedule"; scheduleEl.classList.add("schedule", automationId, "fake-input"); - scheduleEl.setAttribute("data-cron", automation.crontime); - scheduleEl.setAttribute("data-original", automation.schedule); + scheduleEl.setAttribute("data-cron", automationCrontime); + scheduleEl.setAttribute("data-original", automationSchedule); scheduleEl.title = automationNextRun; - scheduleEl.value = automation.schedule; + scheduleEl.value = automationSchedule; // automation query to run input let queryToRunEl = document.createElement("textarea"); queryToRunEl.id = `automation-queryToRun-${automationId}`; queryToRunEl.classList.add("automation-instructions", automationId, "fake-input"); - queryToRunEl.setAttribute("data-original", automation.query_to_run); + queryToRunEl.setAttribute("data-original", automationQueryToRun); queryToRunEl.name = "query-to-run"; - queryToRunEl.textContent = automation.query_to_run; + queryToRunEl.textContent = automationQueryToRun; // Create automation actions section let automationButtonsEl = document.createElement("div"); @@ -576,9 +584,9 @@ <h2 class="section-title"> }); }); // Check if subject, crontime, query_to_run are all filled out. If so, show it as a populated suggested automation. - const subject = "{{ subject }}"; - const crontime = "{{ crontime }}"; - const query = "{{ queryToRun }}"; + const subject = DOMPurify.sanitize("{{ subject }}"); + const crontime = DOMPurify.sanitize("{{ crontime }}"); + const query = DOMPurify.sanitize("{{ queryToRun }}"); if (subject && crontime && query) { const preFilledAutomation = createPreFilledAutomation(subject, crontime, query); @@ -590,9 +598,9 @@ <h2 class="section-title"> listAutomations(); } else { // Check if subject, crontime, query_to_run are all filled out. If so, show it as a populated suggested automation. - const subject = "{{ subject }}"; - const crontime = "{{ crontime }}"; - const query = "{{ queryToRun }}"; + const subject = DOMPurify.sanitize("{{ subject }}"); + const crontime = DOMPurify.sanitize("{{ crontime }}"); + const query = DOMPurify.sanitize("{{ queryToRun }}"); if (subject && crontime && query) { const preFilledAutomation = createPreFilledAutomation(subject, crontime, query); @@ -910,7 +918,8 @@ <h2 class="section-title"> let method = "POST"; if (!create) { - const subject = encodeURIComponent(document.getElementById(`automation-subject-${automationId}`).value); + const subjectEl = document.getElementById(`automation-subject-${automationId}`); + const subject = encodeURIComponent(subjectEl.value); query_string += `&automation_id=${automationId}`; query_string += `&subject=${subject}`; method = "PUT"
1c7a562880eeGenerate automation cards via DOM scripting
1 file changed · +218 −127
src/khoj/interface/web/config_automation.html+218 −127 modified@@ -388,86 +388,132 @@ <h2 class="section-title"> function generateAutomationRow(automation, isSuggested=false) { let automationId = automation.id; let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`; + + // Create card top elements let automationEl = document.createElement("div"); - automationEl.innerHTML = ` - <div class="card automation" id="automation-card-${automationId}"> - <div class="card-header" onclick="onClickEditAutomationCard('${automationId}')"> - <div class="subject-wrapper"> - <input type="text" - id="automation-subject-${automationId}" - class="${automationId} fake-input" - name="subject" - data-original="${automation.subject}" - value="${automation.subject}"> - <img class=automation-share-icon id="share-link-${automationId}" src="/static/assets/icons/share.svg" alt="Share" onclick="copyShareLink(event, '${automationId}', '${automation.subject}', '${automation.crontime}', '${automation.query_to_run}')"> - <img class="automation-edit-icon ${automationId}" src="/static/assets/icons/pencil-edit.svg" onclick="onClickEditAutomationCard('${automationId}')" alt="Automations"> - </div> - <input type="text" - id="automation-schedule-${automationId}" - name="schedule" - class="schedule ${automationId} fake-input" - data-cron="${automation.crontime}" - data-original="${automation.schedule}" - title="${automationNextRun}" - value="${automation.schedule}"> - <textarea id="automation-queryToRun-${automationId}" - class="automation-instructions ${automationId} fake-input" - data-original="${automation.query_to_run}" - name="query-to-run">${automation.query_to_run}</textarea> - ${isSuggested ? - `<img class=promo-image src="${automation.promoImage}" alt="Promo Image">`: - "" - } - </div> - <div id="automation-buttons-wrapper"> - <div class="automation-buttons"> - ${isSuggested ? - `<div id="empty-div"></div> - <div id="empty-div"></div>`: - ` - <button type="button" - class="delete-automation-button negative-button" - id="delete-automation-button-${automationId}">Delete</button> - <button type="button" - class="send-preview-automation-button positive-button" - title="Immediately get a preview of this automation" - onclick="sendAPreviewAutomation('${automationId}')">Preview</button> - ` - } - <button type="button" - class="save-automation-button positive-button" - id="save-automation-button-${automationId}"> - ${isSuggested ? "Add" : "Save"} - </button> - </div> - </div> - <div id="automation-success-${automationId}" style="display: none;"></div> - </div> - `; + let automationCardEl = document.createElement("div"); + automationCardEl.id = `automation-card-${automationId}`; + automationCardEl.classList.add("card", "automation"); + + // Create card header elements + let automationCardFormEl = document.createElement("div"); + automationCardFormEl.className = "card-header"; + + let automationButtonsWrapperEl = document.createElement("div"); + automationButtonsWrapperEl.id = "automation-buttons-wrapper"; + + let automationSuccessEl = document.createElement("div"); + automationSuccessEl.id = `automation-success-${automationId}`; + automationSuccessEl.style.display = "none"; + + // Create automation card form section + automationCardFormEl.onclick = function() { onClickEditAutomationCard(automationId); }; + + // automation subject input + let subjectWrapperEl = document.createElement("div"); + subjectWrapperEl.className = "subject-wrapper"; + let subjectEl = document.createElement("input"); + subjectEl.type = "text"; + subjectEl.id = `automation-subject-${automationId}`; + subjectEl.classList.add(automationId, "fake-input"); + subjectEl.name = "subject"; + subjectEl.setAttribute("data-original", automation.subject); + subjectEl.value = automation.subject; - let automationButtonsSection = automationEl.querySelector(".automation-buttons"); + // automation share link + let shareLinkEl = document.createElement("img"); + shareLinkEl.id = `share-link-${automationId}`, + shareLinkEl.className = "automation-share-icon"; + shareLinkEl.src = "/static/assets/icons/share.svg"; + shareLinkEl.alt = "Share"; + shareLinkEl.onclick = function(event) { copyShareLink(event, automationId, automation.subject, automation.crontime, automation.query_to_run); }; + + // automation edit action + let editIconEl = document.createElement("img"); + editIconEl.classList.add("automation-edit-icon", automationId); + editIconEl.src = "/static/assets/icons/pencil-edit.svg"; + editIconEl.alt = "Automations"; + editIconEl.onclick = function() { onClickEditAutomationCard(automationId); }; + + // automation schedule input + let scheduleEl = document.createElement("input"); + scheduleEl.type = "text"; + scheduleEl.id = `automation-schedule-${automationId}`; + scheduleEl.name = "schedule"; + scheduleEl.classList.add("schedule", automationId, "fake-input"); + scheduleEl.setAttribute("data-cron", automation.crontime); + scheduleEl.setAttribute("data-original", automation.schedule); + scheduleEl.title = automationNextRun; + scheduleEl.value = automation.schedule; + + // automation query to run input + let queryToRunEl = document.createElement("textarea"); + queryToRunEl.id = `automation-queryToRun-${automationId}`; + queryToRunEl.classList.add("automation-instructions", automationId, "fake-input"); + queryToRunEl.setAttribute("data-original", automation.query_to_run); + queryToRunEl.name = "query-to-run"; + queryToRunEl.textContent = automation.query_to_run; + + // Create automation actions section + let automationButtonsEl = document.createElement("div"); + automationButtonsEl.className = "automation-buttons"; if (!isSuggested) { - automationButtonsSection.classList.add("hide-details"); - automationButtonsSection.classList.add(automationId); + automationButtonsEl.classList.add("hide-details", automationId); } - let saveAutomationButtonEl = automationEl.querySelector(`#save-automation-button-${automation.id}`); - saveAutomationButtonEl.addEventListener("click", async () => { await saveAutomation(automation.id, isSuggested); }); - let deleteAutomationButtonEl = automationEl.querySelector(`#delete-automation-button-${automation.id}`); - if (deleteAutomationButtonEl) { - deleteAutomationButtonEl.addEventListener("click", () => { - deleteAutomation(automation.id); - document.getElementById('overlay').style.display = 'none'; - }); + // save automation button + let saveAutomationButtonEl = document.createElement("button"); + saveAutomationButtonEl.type = "button"; + saveAutomationButtonEl.className = "save-automation-button positive-button"; + saveAutomationButtonEl.id = `save-automation-button-${automationId}`; + saveAutomationButtonEl.textContent = isSuggested ? "Add" : "Save"; + saveAutomationButtonEl.onclick = async () => { await saveAutomation(automation.id, isSuggested); }; + + // promo image for suggested automations + let promoImageEl = isSuggested ? document.createElement("img") : null; + if (isSuggested) { + promoImageEl.className = "promo-image"; + promoImageEl.src = automation.promoImage; + promoImageEl.alt = "Promo Image"; } - let cancelEditAutomationButtonEl = automationEl.querySelector(`#cancel-edit-automation-button-${automation.id}`); - if (cancelEditAutomationButtonEl) { - cancelEditAutomationButtonEl.addEventListener("click", (event) => { - clickCancelEdit(event, automation.id); - document.getElementById('overlay').style.display = 'none'; - }); + + // delete automation button + let emptyDivEl = document.createElement("div"); + emptyDivEl.className = "empty-div"; + let deleteAutomationButtonEl = !isSuggested ? document.createElement("button") : emptyDivEl; + if (!isSuggested) { + deleteAutomationButtonEl.type = "button"; + deleteAutomationButtonEl.className = "delete-automation-button negative-button"; + deleteAutomationButtonEl.id = `delete-automation-button-${automationId}`; + deleteAutomationButtonEl.textContent = "Delete"; + deleteAutomationButtonEl.onclick = function() { deleteAutomation(automationId); document.getElementById('overlay').style.display = 'none'; }; + } + + // send preview automation button + emptyDivEl = document.createElement("div"); + emptyDivEl.className = "empty-div"; + let sendPreviewAutomationButtonEl = !isSuggested ? document.createElement("button") : emptyDivEl; + if (!isSuggested) { + sendPreviewAutomationButtonEl.type = "button"; + sendPreviewAutomationButtonEl.className = "send-preview-automation-button positive-button"; + sendPreviewAutomationButtonEl.title = "Immediately get a preview of this automation"; + sendPreviewAutomationButtonEl.textContent = "Preview"; + sendPreviewAutomationButtonEl.onclick = function() { sendAPreviewAutomation(automationId); }; } + // Construct automation card from elements + subjectWrapperEl.append(subjectEl, shareLinkEl, editIconEl); + automationButtonsEl.append(deleteAutomationButtonEl, sendPreviewAutomationButtonEl, saveAutomationButtonEl); + + automationCardFormEl.append(subjectWrapperEl, scheduleEl, queryToRunEl); + if (isSuggested) { + automationCardFormEl.append(promoImageEl); + } + automationButtonsWrapperEl.append(automationButtonsEl); + + automationCardEl.append(automationCardFormEl, automationButtonsWrapperEl, automationSuccessEl); + automationEl.append(automationCardEl); + return automationEl.firstElementChild; } @@ -560,9 +606,6 @@ <h2 class="section-title"> }); } - - - function listSuggestedAutomations() { const SuggestedAutomationsList = document.getElementById("suggested-automations-list"); SuggestedAutomationsList.innerHTML = ''; // Clear existing content @@ -830,7 +873,6 @@ <h2 class="section-title"> return; } - } // Get client location information from IP @@ -887,6 +929,8 @@ <h2 class="section-title"> }) .then(response => response.ok ? response.json() : Promise.reject(data)) .then(automation => { + // Remove modal overlay + document.getElementById('overlay').style.display = 'none'; if (create) { const automationEl = document.getElementById(`automation-card-${automationId}`); // Create a more interesting confirmation animation. @@ -936,66 +980,113 @@ <h2 class="section-title"> }); } - const create_automation_button = document.getElementById("create-automation-button"); - create_automation_button.addEventListener("click", function(event) { - event.preventDefault(); - var automationEl = document.createElement("div"); - automationEl.classList.add("card"); - automationEl.classList.add("automation"); - automationEl.classList.add("new-automation") - const placeholderId = Date.now(); + function createAutomationEl(placeholderId = null) { + let automationEl = document.createElement("div"); + automationEl.classList.add("card", "automation", "new-automation"); + placeholderId = placeholderId ?? `automation_${Date.now()}`; automationEl.id = "automation-card-" + placeholderId; - var scheduleSelector = createScheduleSelector(placeholderId); - automationEl.innerHTML = ` - <label for="schedule">New Automation</label> - ${scheduleSelector.outerHTML} - <label for="query-to-run">What would you like to receive in your automation?</label> - <textarea id="automation-queryToRun-${placeholderId}" placeholder="Provide me with a mindful moment, reminding me to be centered."></textarea> - <div class="automation-buttons"> - <button type="button" - class="delete-automation-button negative-button" - onclick="deleteAutomation(${placeholderId}, true)" - id="delete-automation-button-${placeholderId}">Cancel</button> - <button type="button" - class="save-automation-button" - onclick="saveAutomation(${placeholderId}, true)" - id="save-automation-button-${placeholderId}">Create</button> - </div> - <div id="automation-success-${placeholderId}" style="display: none;"></div> - `; + + // Create label for schedule + let scheduleLabel = document.createElement("label"); + scheduleLabel.setAttribute("for", "schedule"); + scheduleLabel.textContent = "New Automation"; + + // Create schedule selector + let scheduleSelector = createScheduleSelector(placeholderId); + + // Create label for query-to-run + let queryLabel = document.createElement("label"); + queryLabel.setAttribute("for", "query-to-run"); + queryLabel.textContent = "What would you like to receive in your automation?"; + + // Create textarea for query-to-run + let queryTextarea = document.createElement("textarea"); + queryTextarea.id = `automation-queryToRun-${placeholderId}`; + queryTextarea.classList.add(`automation-queryToRun-${placeholderId}`); + queryTextarea.placeholder = "Provide me with a mindful moment, reminding me to be centered."; + + // Create buttons container + let buttonsContainer = document.createElement("div"); + buttonsContainer.classList.add("automation-buttons"); + + // Create cancel button + let deleteButton = document.createElement("button"); + deleteButton.type = "button"; + deleteButton.classList.add("delete-automation-button", "negative-button"); + deleteButton.textContent = "Cancel"; + deleteButton.id = `delete-automation-button-${placeholderId}`; + deleteButton.onclick = () => deleteAutomation(placeholderId, true); + + // Create save button + let saveButton = document.createElement("button"); + saveButton.type = "button"; + saveButton.classList.add("save-automation-button"); + saveButton.textContent = "Create"; + saveButton.id = `save-automation-button-${placeholderId}`; + saveButton.onclick = () => saveAutomation(placeholderId, true); + + // Create success message container + let successMessage = document.createElement("div"); + successMessage.id = `automation-success-${placeholderId}`; + successMessage.style.display = "none"; + + // Append schedule label and selector + automationEl.appendChild(scheduleLabel); + automationEl.appendChild(scheduleSelector); + + // Append query label and textarea + automationEl.appendChild(queryLabel); + automationEl.appendChild(queryTextarea); + + // Append buttons to their container + buttonsContainer.appendChild(deleteButton); + buttonsContainer.appendChild(saveButton); + + // Append buttons container to automationEl + automationEl.appendChild(buttonsContainer); + + // Append success message to automationEl + automationEl.appendChild(successMessage); + + return automationEl; + } + + const createAutomationButtonEl = document.getElementById("create-automation-button"); + createAutomationButtonEl.addEventListener("click", function(event) { + event.preventDefault(); + + // Insert automationEl into the DOM + let placeholderId = `automation_${Date.now()}`; + let automationEl = createAutomationEl(placeholderId); document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild); + setupScheduleViewListener("* * * * *", placeholderId); }) function createPreFilledAutomation(subject, crontime, query) { document.getElementById('overlay').style.display = 'block'; - var automationEl = document.createElement("div"); - automationEl.classList.add("card"); - automationEl.classList.add("automation"); - automationEl.classList.add("new-automation") - const placeholderId = Date.now(); + + let placeholderId = `automation_${Date.now()}`; + let automationEl = createAutomationEl(placeholderId); + + // Configure automationEl with pre-filled values automationEl.classList.add(`${placeholderId}`); - automationEl.id = "automation-card-" + placeholderId; - var scheduleSelector = createScheduleSelector(placeholderId); - automationEl.innerHTML = ` - <label for="subject">New Automation</label> - <input type="text" id="automation-subject-${placeholderId}" value="${subject}"> - ${scheduleSelector.outerHTML} - <label for="query-to-run">What would you like to receive in your automation?</label> - <textarea id="automation-queryToRun-${placeholderId}">${query}</textarea> - <div class="automation-buttons"> - <button type="button" - class="delete-automation-button negative-button" - onclick="deleteAutomation(${placeholderId}, true)" - id="delete-automation-button-${placeholderId}">Cancel</button> - <button type="button" - class="save-automation-button" - onclick="saveAutomation(${placeholderId}, true)" - id="save-automation-button-${placeholderId}">Create</button> - </div> - <div id="automation-success-${placeholderId}" style="display: none;"></div> - `; + automationEl.getElementsByClassName(`automation-queryToRun-${placeholderId}`)[0].value = query; + + // Create input for subject + let subjectEl = document.createElement("input"); + subjectEl.type = "text"; + subjectEl.id = `automation-subject-${placeholderId}`; + subjectEl.value = subject; + + // Insert subjectEl after label for subject + let subjectLabel = automationEl.querySelector(`label[for="automation-subject-${placeholderId}"]`); + automationEl.firstChild.insertAdjacentElement('afterend', subjectEl); + automationEl.firstChild.label = "subject"; + + // Insert automationEl into the DOM document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild); + setupScheduleViewListener(crontime, placeholderId); }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-cf72-vg59-4j4hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-43396ghsaADVISORY
- github.com/khoj-ai/khoj/commit/1c7a562880eeb7354325545d2cf6c5d1d1134812ghsax_refsource_MISCWEB
- github.com/khoj-ai/khoj/commit/55be90cdd2f9d6a09c8bf9ceea52fc36b9201626ghsax_refsource_MISCWEB
- github.com/khoj-ai/khoj/security/advisories/GHSA-cf72-vg59-4j4hghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.