Summary
Versions of tf2-item-format since at least 4.2.6 are vulnerable to a Regular Expression Denial of Service (ReDoS) attack when parsing crafted user input.
Tested Versions
5.9.13
5.8.10
5.7.0
5.6.17
4.3.5
4.2.6
Mitigation
v5
Upgrade package to ^5.9.14
v4
No patch exists. Please consult the v4 to v5 migration guide to upgrade to v5.
If upgrading to v5 is not possible, fork the module repository and implement the fix detailed below.
Details
All mentions of source code are based on the 5.9.13 version of the package.
The error originates from src/shared/decomposeName.ts Lines 54-57:
itemName = itemName.replace(
new RegExp(`(( ${toRemove})|(${toRemove} ))`),
''
);
since both itemName and toRemove originate from user input, this can be exploited to perform a ReDoS attack.
This ReGeX is only called under the following conditions:
attributes.usableItem is truthy
- either
attributes.usableItem.output or attributes.usableItem.target are falsy
under these conditions, itemName is is unmodified from user input, and toRemove is created by the getUsableItemToRemove() function, which can return one of three possible strings:
"{attributes.usableItem.target}"
"{attributes.usableItem.target.output}"
"{attributes.usableItem.outputQuality} {usableItem.output}"
decomposeName() is used on two API functions: parseString() (src/parseString.ts) and ParsedEcon.itemName.getShort() (src/parseEconItem/ParsedEcon/ItemName.ts). Since the latter typically only handles input from Steam, it was not investigated as a potential attack vector.
When parseString() is called, the attributes object is created from the constructor of the Attributes class, where the usableItem field is populated by getUsableItem() (src/parseString/Attributes/getUsableItem.ts).
Since malicious payloads need to ensure either attributes.usableItem.output or attributes.usableItem.target are falsy, either isChemistrySet() or getItemIfTarget() must not return a falsy value. Since the getItemIfTarget() was easier to exploit, it was used in the PoC. It is highly possible that isChemistrySet() is also vulnerable to a similar attack.
src/parseString/Attributes/getUsableItem.ts Lines 34-45
const item = getItemIfTarget(name);
if (item) {
return {
target: name
.replace(` ${item}`, '')
.replace(`${getKillstreak(name)} `, '')
// Incase its uncraftable
.replace('Non-Craftable ', '')
// For Unusualifiers
.replace('Unusual ', ''),
};
}
Here, name is unmodifed user input.
If an input of form "<anything> Kit" is passed to getItemIfTarget(), the function will return "Kit", which will result in attributes.usableItem.target being set to "<anything>" and attributes.usableItem.output being undefined.
Now, since attributes.usableItem.target is defined, getUsableItemToRemove() will return "<anything>", setting toRemove to "<anything>".
In short, due to the above, a payload of form "<anything> Kit" cause a Regex of form
/(( <anything>)|(<anything> ))/ to be created and executed on "<anything> Kit".
This exposes the possibility of ReDoS through catastrophic backtracking.
If <anything> is set a specially crafted payload, the Regex will take an exponential amount of time to execute, causing the server to hang.
For the PoC, the payload )|^.{23}(a+)+$| Kit |( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZ
was used.
)| is used to isolate the malicious pattern
^.{23} causes the malicious pattern to ignore itself and "Kit"
(a+)+$ This is a malicious Regex pattern that will cause catastrophic backtracking upon encountering a string of form aaa...aaaZ
| is used to isolate the malicious pattern
Kit is used to ensure this payload is converted to Regex as shown above.
|( is used to avoid causing a Regex syntax error.
a space is used to make sure the aaa...aaaZ cannot match itself, as this create a group.
aaa...aaaZ is used to trigger the catastrophic backtracking.
PoC
v4
const { parseString } = require( "tf2-item-format");
const payload = ")|^.{23}(a+)+$| Kit |( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZ";
parseString(payload , true, true);
v5
const { parseString } = require( "tf2-item-format/static");
const payload = ")|^.{23}(a+)+$| Kit |( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZ";
parseString(payload , true, true);
Remediation
In src/shared/decomposeName.ts Lines 54-57:
itemName = itemName.replace(
new RegExp(`(( ${toRemove})|(${toRemove} ))`),
''
);
should be updated to
itemName = itemName.replace(`${toRemove} `, '');
itemName = itemName.replace(` ${toRemove}`, '');
Impact
This vulnerability can be exploited by an attacker to perform DoS attacks on any service that uses any tf2-item-format to parse user input.
Credit
Summary
Versions of
tf2-item-formatsince at least4.2.6are vulnerable to a Regular Expression Denial of Service (ReDoS) attack when parsing crafted user input.Tested Versions
5.9.135.8.105.7.05.6.174.3.54.2.6Mitigation
v5
Upgrade package to
^5.9.14v4
No patch exists. Please consult the v4 to v5 migration guide to upgrade to v5.
If upgrading to v5 is not possible, fork the module repository and implement the fix detailed below.
Details
The error originates from
src/shared/decomposeName.tsLines 54-57:since both
itemNameandtoRemoveoriginate from user input, this can be exploited to perform a ReDoS attack.This ReGeX is only called under the following conditions:
attributes.usableItemis truthyattributes.usableItem.outputorattributes.usableItem.targetare falsyunder these conditions,
itemNameis is unmodified from user input, andtoRemoveis created by thegetUsableItemToRemove()function, which can return one of three possible strings:"{attributes.usableItem.target}""{attributes.usableItem.target.output}""{attributes.usableItem.outputQuality} {usableItem.output}"decomposeName()is used on two API functions:parseString()(src/parseString.ts) andParsedEcon.itemName.getShort()(src/parseEconItem/ParsedEcon/ItemName.ts). Since the latter typically only handles input from Steam, it was not investigated as a potential attack vector.When
parseString()is called, theattributesobject is created from the constructor of theAttributesclass, where theusableItemfield is populated bygetUsableItem()(src/parseString/Attributes/getUsableItem.ts).Since malicious payloads need to ensure either
attributes.usableItem.outputorattributes.usableItem.targetare falsy, eitherisChemistrySet()orgetItemIfTarget()must not return a falsy value. Since thegetItemIfTarget()was easier to exploit, it was used in the PoC. It is highly possible thatisChemistrySet()is also vulnerable to a similar attack.Here,
nameis unmodifed user input.If an input of form
"<anything> Kit"is passed togetItemIfTarget(), the function will return"Kit", which will result inattributes.usableItem.targetbeing set to"<anything>"andattributes.usableItem.outputbeingundefined.Now, since
attributes.usableItem.targetis defined,getUsableItemToRemove()will return"<anything>", settingtoRemoveto"<anything>".In short, due to the above, a payload of form
"<anything> Kit"cause a Regex of form/(( <anything>)|(<anything> ))/to be created and executed on"<anything> Kit".This exposes the possibility of ReDoS through catastrophic backtracking.
If
<anything>is set a specially crafted payload, the Regex will take an exponential amount of time to execute, causing the server to hang.For the PoC, the payload
)|^.{23}(a+)+$| Kit |( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZwas used.
)|is used to isolate the malicious pattern^.{23}causes the malicious pattern to ignore itself and"Kit"(a+)+$This is a malicious Regex pattern that will cause catastrophic backtracking upon encountering a string of formaaa...aaaZ|is used to isolate the malicious patternKitis used to ensure this payload is converted to Regex as shown above.|(is used to avoid causing a Regex syntax error.a space is used to make sure theaaa...aaaZcannot match itself, as this create a group.aaa...aaaZis used to trigger the catastrophic backtracking.PoC
v4
v5
Remediation
should be updated to
Impact
This vulnerability can be exploited by an attacker to perform DoS attacks on any service that uses any
tf2-item-formatto parse user input.Credit