Skip to content
/ noc Public

Commit 659272a

Browse files
authored
Security: Patch prototype pollution vulnerability for metricaction (#30)
1 parent 8028e20 commit 659272a

File tree

6 files changed

+122
-3480
lines changed

6 files changed

+122
-3480
lines changed

ui/pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/vitest.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {defineConfig} from "vitest/config"
2+
3+
export default defineConfig({
4+
test: {
5+
environment: "node",
6+
globals: true,
7+
include: ["**/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}", "**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
8+
},
9+
})

ui/web/package.json

Lines changed: 0 additions & 60 deletions
This file was deleted.

ui/web/pm/metricaction/Application.js

Lines changed: 110 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -617,41 +617,12 @@ Ext.define("NOC.pm.metricaction.Application", {
617617
//
618618
saveRecord: function(data){
619619
var me = this,
620-
save = {},
621620
inputs = Ext.Array.push([], {
622621
metric_type: data.metric_type0,
623622
}, Ext.Array.map(me.query("[name=metric_type]"), function(input){
624623
return {metric_type: input.getValue()}
625624
})),
626-
set = function(path, value){
627-
let keys = path.split("."),
628-
curStep = save,
629-
keysForSkipping = ["__proto__", "constructor", "prototype", "__label"],
630-
isKeyForSkipping = keysForSkipping.some(function(key){
631-
return path.indexOf(key) !== -1;
632-
});
633-
634-
if(isKeyForSkipping){
635-
return;
636-
}
637-
for(var i = 0; i < keys.length - 1; i++){
638-
var key = keys[i];
639-
640-
if(!curStep[key] && !Object.hasOwn(curStep, key)){
641-
var nextKey = keys[i + 1];
642-
var useArray = /^\+?(0|[1-9]\d*)$/.test(nextKey);
643-
curStep[key] = useArray ? [] : {};
644-
}
645-
curStep = curStep[key];
646-
}
647-
var finalStep = keys[keys.length - 1];
648-
curStep[finalStep] = value;
649-
};
650-
651-
Ext.Object.each(data, set);
652-
// set(key, value);
653-
// });
654-
625+
save = me.transferFlatToNested(data);
655626
save["compose_inputs"] = inputs;
656627

657628
me.mask("Saving ...");
@@ -660,7 +631,7 @@ Ext.define("NOC.pm.metricaction.Application", {
660631
url: me.base_url + (me.currentRecord ? me.currentRecord.id + "/" : ""),
661632
method: me.currentRecord ? "PUT" : "POST",
662633
scope: me,
663-
jsonData: save,
634+
jsonData: JSON.stringify(save),
664635
success: function(response){
665636
// Process result
666637
var data = Ext.decode(response.responseText);
@@ -694,6 +665,114 @@ Ext.define("NOC.pm.metricaction.Application", {
694665
});
695666
},
696667
//
668+
transferFlatToNested: function(flatData){
669+
const result = Object.create(null),
670+
DANGEROUS_KEYS = new Set([
671+
"__proto__",
672+
"constructor",
673+
"prototype",
674+
"__defineGetter__",
675+
"__defineSetter__",
676+
"__lookupGetter__",
677+
"__lookupSetter__",
678+
"hasOwnProperty",
679+
"isPrototypeOf",
680+
"propertyIsEnumerable",
681+
"toString",
682+
"valueOf",
683+
]);
684+
let isSafeKey = function(key){
685+
return !DANGEROUS_KEYS.has(key) &&
686+
typeof key === "string" &&
687+
key.length > 0;
688+
},
689+
isSafePath = function(path){
690+
if(typeof path !== "string" || path.length === 0){
691+
return false;
692+
}
693+
694+
const segments = path.split(".");
695+
return segments.every(segment => {
696+
if(DANGEROUS_KEYS.has(segment)){
697+
return false;
698+
}
699+
700+
if(segment.includes("__")){ // Prevent double underscore in segment names
701+
return false;
702+
}
703+
704+
return true;
705+
});
706+
},
707+
safeSetProperty = function(obj, key, value){
708+
if(!isSafeKey(key)){
709+
console.warn(`Attempted to set dangerous property: ${key}`);
710+
return;
711+
}
712+
713+
// Используем Object.defineProperty для большей безопасности
714+
Object.defineProperty(obj, key, {
715+
value: value,
716+
writable: true,
717+
enumerable: true,
718+
configurable: true,
719+
});
720+
},
721+
setNestedValue = function(path, value, target){
722+
if(!isSafePath(path)){ // Dangerous path detected and ignored
723+
return;
724+
}
725+
726+
const segments = path.split(".");
727+
let current = target;
728+
729+
for(let i = 0; i < segments.length - 1; i++){
730+
const segment = segments[i];
731+
732+
if(!isSafeKey(segment)){ // Dangerous segment in path detected and ignored
733+
return;
734+
}
735+
736+
if(!Object.hasOwn(current, segment)){
737+
const nextSegment = segments[i + 1],
738+
isNumericIndex = /^\d+$/.test(nextSegment),
739+
newContainer = isNumericIndex ?
740+
[] :
741+
Object.create(null);
742+
743+
safeSetProperty(current, segment, newContainer);
744+
}
745+
746+
current = current[segment];
747+
if(current === null || typeof current !== "object"){
748+
console.warn(`Invalid intermediate object at path: ${path}`);
749+
return;
750+
}
751+
}
752+
753+
const finalSegment = segments[segments.length - 1];
754+
if(isSafeKey(finalSegment)){
755+
safeSetProperty(current, finalSegment, value);
756+
}
757+
};
758+
if(!flatData || typeof flatData !== "object"){
759+
return result;
760+
}
761+
762+
for(const [key, value] of Object.entries(flatData)){
763+
if(!isSafeKey(key) || key.endsWith("__label")){
764+
continue;
765+
}
766+
767+
if(key.includes(".")){
768+
setNestedValue(key, value, result);
769+
} else{
770+
safeSetProperty(result, key, value);
771+
}
772+
}
773+
return result;
774+
},
775+
//
697776
addInput: function(value){
698777
var me = this,
699778
inputsContainer = me.down("[itemId=input-container]"),

0 commit comments

Comments
 (0)