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-format
since at least4.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
The error originates from
src/shared/decomposeName.ts
Lines 54-57:since both
itemName
andtoRemove
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 truthyattributes.usableItem.output
orattributes.usableItem.target
are falsyunder these conditions,
itemName
is is unmodified from user input, andtoRemove
is 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, theattributes
object is created from the constructor of theAttributes
class, where theusableItem
field is populated bygetUsableItem()
(src/parseString/Attributes/getUsableItem.ts
).Since malicious payloads need to ensure either
attributes.usableItem.output
orattributes.usableItem.target
are 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,
name
is unmodifed user input.If an input of form
"<anything> Kit"
is passed togetItemIfTarget()
, the function will return"Kit"
, which will result inattributes.usableItem.target
being set to"<anything>"
andattributes.usableItem.output
beingundefined
.Now, since
attributes.usableItem.target
is defined,getUsableItemToRemove()
will return"<anything>"
, settingtoRemove
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 formaaa...aaaZ
|
is used to isolate the malicious patternKit
is used to ensure this payload is converted to Regex as shown above.|(
is used to avoid causing a Regex syntax error.aaa...aaaZ
cannot match itself, as this create a group.aaa...aaaZ
is 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-format
to parse user input.Credit