mirror of
https://github.com/github/codeql.git
synced 2026-04-29 18:55:14 +02:00
Merge pull request #4942 from esbena/js/reintroduce-resource-exhaustion
Approved by erik-krogh
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
|
||||
<p>
|
||||
|
||||
Applications are constrained by how many resources they can make use
|
||||
of. Failing to respect these constraints may cause the application to
|
||||
be unresponsive or crash. It is therefore problematic if attackers
|
||||
can control the sizes or lifetimes of allocated objects.
|
||||
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
|
||||
Ensure that attackers can not control object sizes and their
|
||||
lifetimes. If object sizes and lifetimes must be controlled by
|
||||
external parties, ensure you restrict the object sizes and lifetimes so that
|
||||
they are within acceptable ranges.
|
||||
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
|
||||
<p>
|
||||
|
||||
The following example lets a user choose a delay after
|
||||
which a function is executed:
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/ResourceExhaustion_timeout.js" />
|
||||
|
||||
<p>
|
||||
|
||||
This is problematic because a large delay essentially makes the
|
||||
application wait indefinitely before executing the function. Repeated
|
||||
registrations of such delays will therefore use up all of the memory
|
||||
in the application.
|
||||
|
||||
A limit on the delay will prevent the attack:
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/ResourceExhaustion_timeout_fixed.js" />
|
||||
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @name Resource exhaustion
|
||||
* @description Allocating objects or timers with user-controlled
|
||||
* sizes or durations can cause resource exhaustion.
|
||||
* @kind path-problem
|
||||
* @problem.severity warning
|
||||
* @id js/resource-exhaustion
|
||||
* @precision high
|
||||
* @tags security
|
||||
* external/cwe/cwe-770
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow::PathGraph
|
||||
import experimental.semmle.javascript.security.dataflow.ResourceExhaustion::ResourceExhaustion
|
||||
|
||||
from Configuration dataflow, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where dataflow.hasFlowPath(source, sink)
|
||||
select sink, source, sink, sink.getNode().(Sink).getProblemDescription() + " from $@.", source,
|
||||
"here"
|
||||
@@ -0,0 +1,9 @@
|
||||
var http = require("http"),
|
||||
url = require("url");
|
||||
|
||||
var server = http.createServer(function(req, res) {
|
||||
var delay = parseInt(url.parse(req.url, true).query.delay);
|
||||
|
||||
setTimeout(f, delay); // BAD
|
||||
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
var http = require("http"),
|
||||
url = require("url");
|
||||
|
||||
var server = http.createServer(function(req, res) {
|
||||
var delay = parseInt(url.parse(req.url, true).query.delay);
|
||||
|
||||
if (delay > 1000) {
|
||||
res.statusCode = 400;
|
||||
res.end("Bad request.");
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(f, delay); // GOOD
|
||||
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Provides a taint tracking configuration for reasoning about
|
||||
* resource exhaustion vulnerabilities (CWE-770).
|
||||
*
|
||||
* Note, for performance reasons: only import this file if
|
||||
* `ResourceExhaustion::Configuration` is needed, otherwise
|
||||
* `ResourceExhaustionCustomizations` should be imported instead.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import semmle.javascript.security.dataflow.LoopBoundInjectionCustomizations
|
||||
|
||||
module ResourceExhaustion {
|
||||
import ResourceExhaustionCustomizations::ResourceExhaustion
|
||||
|
||||
/**
|
||||
* A data flow configuration for resource exhaustion vulnerabilities.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "ResourceExhaustion" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof Source }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node src, DataFlow::Node dst) {
|
||||
isNumericFlowStep(src, dst)
|
||||
or
|
||||
// reuse most existing taint steps
|
||||
isRestrictedAdditionalTaintStep(src, dst)
|
||||
}
|
||||
|
||||
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
|
||||
guard instanceof LoopBoundInjection::LengthCheckSanitizerGuard or
|
||||
guard instanceof UpperBoundsCheckSanitizerGuard
|
||||
}
|
||||
}
|
||||
|
||||
predicate isRestrictedAdditionalTaintStep(DataFlow::Node src, DataFlow::Node dst) {
|
||||
any(TaintTracking::AdditionalTaintStep dts).step(src, dst) and
|
||||
not dst.asExpr() instanceof AddExpr and
|
||||
not dst.(DataFlow::MethodCallNode).calls(src, "toString")
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `src` to `dst` as a number.
|
||||
*/
|
||||
predicate isNumericFlowStep(DataFlow::Node src, DataFlow::Node dst) {
|
||||
// steps that introduce or preserve a number
|
||||
dst.(DataFlow::PropRead).accesses(src, ["length", "size"])
|
||||
or
|
||||
exists(DataFlow::CallNode c |
|
||||
c = dst and
|
||||
src = c.getAnArgument()
|
||||
|
|
||||
c = DataFlow::globalVarRef("Math").getAMemberCall(_) or
|
||||
c = DataFlow::globalVarRef(["Number", "parseInt", "parseFloat"]).getACall()
|
||||
)
|
||||
or
|
||||
exists(Expr dstExpr, Expr srcExpr |
|
||||
dstExpr = dst.asExpr() and
|
||||
srcExpr = src.asExpr()
|
||||
|
|
||||
dstExpr.(BinaryExpr).getAnOperand() = srcExpr and
|
||||
not dstExpr instanceof AddExpr
|
||||
or
|
||||
dstExpr.(PlusExpr).getOperand() = srcExpr
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for reasoning about
|
||||
* resource exhaustion vulnerabilities, as well as extension points for
|
||||
* adding your own.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
module ResourceExhaustion {
|
||||
/**
|
||||
* A data flow source for resource exhaustion vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for resource exhaustion vulnerabilities.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node {
|
||||
/**
|
||||
* Gets a description of why this is a problematic sink.
|
||||
*/
|
||||
abstract string getProblemDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow sanitizer for resource exhaustion vulnerabilities.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer that blocks taint flow if the size of a number is limited.
|
||||
*/
|
||||
class UpperBoundsCheckSanitizerGuard extends TaintTracking::SanitizerGuardNode,
|
||||
DataFlow::ValueNode {
|
||||
override RelationalComparison astNode;
|
||||
|
||||
override predicate sanitizes(boolean outcome, Expr e) {
|
||||
true = outcome and
|
||||
e = astNode.getLesserOperand()
|
||||
or
|
||||
false = outcome and
|
||||
e = astNode.getGreaterOperand()
|
||||
}
|
||||
}
|
||||
|
||||
/** A source of remote user input, considered as a data flow source for resource exhaustion vulnerabilities. */
|
||||
class RemoteFlowSourceAsSource extends Source {
|
||||
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
|
||||
}
|
||||
|
||||
/**
|
||||
* A node that determines the repetitions of a string, considered as a data flow sink for resource exhaustion vulnerabilities.
|
||||
*/
|
||||
class StringRepetitionSink extends Sink {
|
||||
StringRepetitionSink() {
|
||||
exists(DataFlow::MethodCallNode repeat |
|
||||
repeat.getMethodName() = "repeat" and
|
||||
this = repeat.getArgument(0)
|
||||
)
|
||||
}
|
||||
|
||||
override string getProblemDescription() {
|
||||
result = "This creates a string with a user-controlled length"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A node that determines the duration of a timer, considered as a data flow sink for resource exhaustion vulnerabilities.
|
||||
*/
|
||||
class TimerDurationSink extends Sink {
|
||||
TimerDurationSink() {
|
||||
this = DataFlow::globalVarRef(["setTimeout", "setInterval"]).getACall().getArgument(1) or
|
||||
this = LodashUnderscore::member(["delay", "throttle", "debounce"]).getACall().getArgument(1)
|
||||
}
|
||||
|
||||
override string getProblemDescription() {
|
||||
result = "This creates a timer with a user-controlled duration"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
nodes
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:6:5:59 | delay |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:14:5:59 | parseIn ... .delay) |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:46 | url.par ... , true) |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:52 | url.par ... ).query |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:58 | url.par ... y.delay |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay |
|
||||
| resource-exhaustion.js:9:7:9:42 | s |
|
||||
| resource-exhaustion.js:9:11:9:34 | url.par ... , true) |
|
||||
| resource-exhaustion.js:9:11:9:40 | url.par ... ).query |
|
||||
| resource-exhaustion.js:9:11:9:42 | url.par ... query.s |
|
||||
| resource-exhaustion.js:9:21:9:27 | req.url |
|
||||
| resource-exhaustion.js:9:21:9:27 | req.url |
|
||||
| resource-exhaustion.js:10:7:10:21 | n |
|
||||
| resource-exhaustion.js:10:11:10:21 | parseInt(s) |
|
||||
| resource-exhaustion.js:10:20:10:20 | s |
|
||||
| resource-exhaustion.js:38:12:38:12 | n |
|
||||
| resource-exhaustion.js:38:12:38:12 | n |
|
||||
| resource-exhaustion.js:39:12:39:12 | s |
|
||||
| resource-exhaustion.js:39:12:39:12 | s |
|
||||
| resource-exhaustion.js:85:17:85:17 | n |
|
||||
| resource-exhaustion.js:85:17:85:17 | n |
|
||||
| resource-exhaustion.js:86:17:86:17 | s |
|
||||
| resource-exhaustion.js:86:17:86:17 | s |
|
||||
| resource-exhaustion.js:87:18:87:18 | n |
|
||||
| resource-exhaustion.js:87:18:87:18 | n |
|
||||
| resource-exhaustion.js:88:18:88:18 | s |
|
||||
| resource-exhaustion.js:88:18:88:18 | s |
|
||||
edges
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:6:5:59 | delay | documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:6:5:59 | delay | documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:14:5:59 | parseIn ... .delay) | documentaion-examples/ResourceExhaustion_timeout.js:5:6:5:59 | delay |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:46 | url.par ... , true) | documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:52 | url.par ... ).query |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:52 | url.par ... ).query | documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:58 | url.par ... y.delay |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:58 | url.par ... y.delay | documentaion-examples/ResourceExhaustion_timeout.js:5:14:5:59 | parseIn ... .delay) |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url | documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:46 | url.par ... , true) |
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url | documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:46 | url.par ... , true) |
|
||||
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:10:20:10:20 | s |
|
||||
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:39:12:39:12 | s |
|
||||
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:39:12:39:12 | s |
|
||||
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:86:17:86:17 | s |
|
||||
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:86:17:86:17 | s |
|
||||
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:88:18:88:18 | s |
|
||||
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:88:18:88:18 | s |
|
||||
| resource-exhaustion.js:9:11:9:34 | url.par ... , true) | resource-exhaustion.js:9:11:9:40 | url.par ... ).query |
|
||||
| resource-exhaustion.js:9:11:9:40 | url.par ... ).query | resource-exhaustion.js:9:11:9:42 | url.par ... query.s |
|
||||
| resource-exhaustion.js:9:11:9:42 | url.par ... query.s | resource-exhaustion.js:9:7:9:42 | s |
|
||||
| resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:9:11:9:34 | url.par ... , true) |
|
||||
| resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:9:11:9:34 | url.par ... , true) |
|
||||
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:38:12:38:12 | n |
|
||||
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:38:12:38:12 | n |
|
||||
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:85:17:85:17 | n |
|
||||
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:85:17:85:17 | n |
|
||||
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:87:18:87:18 | n |
|
||||
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:87:18:87:18 | n |
|
||||
| resource-exhaustion.js:10:11:10:21 | parseInt(s) | resource-exhaustion.js:10:7:10:21 | n |
|
||||
| resource-exhaustion.js:10:20:10:20 | s | resource-exhaustion.js:10:11:10:21 | parseInt(s) |
|
||||
#select
|
||||
| documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay | documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url | documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay | This creates a timer with a user-controlled duration from $@. | documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url | here |
|
||||
| resource-exhaustion.js:38:12:38:12 | n | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:38:12:38:12 | n | This creates a string with a user-controlled length from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
|
||||
| resource-exhaustion.js:39:12:39:12 | s | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:39:12:39:12 | s | This creates a string with a user-controlled length from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
|
||||
| resource-exhaustion.js:85:17:85:17 | n | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:85:17:85:17 | n | This creates a timer with a user-controlled duration from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
|
||||
| resource-exhaustion.js:86:17:86:17 | s | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:86:17:86:17 | s | This creates a timer with a user-controlled duration from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
|
||||
| resource-exhaustion.js:87:18:87:18 | n | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:87:18:87:18 | n | This creates a timer with a user-controlled duration from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
|
||||
| resource-exhaustion.js:88:18:88:18 | s | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:88:18:88:18 | s | This creates a timer with a user-controlled duration from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE-770/ResourceExhaustion.ql
|
||||
@@ -0,0 +1,9 @@
|
||||
var http = require("http"),
|
||||
url = require("url");
|
||||
|
||||
var server = http.createServer(function(req, res) {
|
||||
var delay = parseInt(url.parse(req.url, true).query.delay);
|
||||
|
||||
setTimeout(f, delay); // BAD
|
||||
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
var http = require("http"),
|
||||
url = require("url");
|
||||
|
||||
var server = http.createServer(function(req, res) {
|
||||
var delay = parseInt(url.parse(req.url, true).query.delay);
|
||||
|
||||
if (delay > 1000) {
|
||||
res.statusCode = 400;
|
||||
res.end("Bad request.");
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(f, delay); // GOOD
|
||||
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
// this file contains many `NOT OK [INCONSISTENCY]` annotations, those
|
||||
// would be resolved if the query used flow labels to recognice
|
||||
// numbers flowing to sinks
|
||||
|
||||
var http = require("http"),
|
||||
url = require("url");
|
||||
|
||||
var server = http.createServer(function(req, res) {
|
||||
let s = url.parse(req.url, true).query.s;
|
||||
let n = parseInt(s);
|
||||
|
||||
Buffer.from(s); // OK
|
||||
Buffer.from(n); // OK
|
||||
Buffer.from(x, n); // OK
|
||||
Buffer.from(x, y, s); // NOT OK
|
||||
Buffer.from(x, y, n); // NOT OK [INCONSISTENCY]
|
||||
Buffer.from(x, y, n); // NOT OK [INCONSISTENCY]
|
||||
Buffer.alloc(n); // NOT OK [INCONSISTENCY]
|
||||
Buffer.allocUnsafe(n); // NOT OK [INCONSISTENCY]
|
||||
Buffer.allocUnsafeSlow(n); // NOT OK [INCONSISTENCY]
|
||||
|
||||
new Buffer(n); // NOT OK [INCONSISTENCY]
|
||||
new Buffer(x, n); // OK
|
||||
new Buffer(x, y, n); // NOT OK [INCONSISTENCY]
|
||||
|
||||
new SlowBuffer(n); // NOT OK [INCONSISTENCY]
|
||||
|
||||
Array(n); // OK
|
||||
new Array(n); // OK
|
||||
|
||||
Array(n).map(); // NOT OK [INCONSISTENCY]
|
||||
new Array(n).map(); // NOT OK [INCONSISTENCY]
|
||||
Array(n).fill(); // NOT OK [INCONSISTENCY]
|
||||
Array(n).join(); // NOT OK [INCONSISTENCY]
|
||||
Array(n).toString(); // NOT OK [INCONSISTENCY]
|
||||
Array(n) + x; // NOT OK [INCONSISTENCY]
|
||||
|
||||
x.repeat(n); // NOT OK
|
||||
x.repeat(s); // NOT OK
|
||||
|
||||
new Buffer(n * x); // NOT OK [INCONSISTENCY]
|
||||
new Buffer(n + n); // NOT OK [INCONSISTENCY]
|
||||
new Buffer(n + x); // OK (maybe)
|
||||
new Buffer(n + s); // OK (this is a string if `s` is a string)
|
||||
new Buffer(s + 2); // OK (this is a string if `s` is a string)
|
||||
new Buffer(s + s); // OK
|
||||
new Buffer(n + "X"); // OK
|
||||
|
||||
new Buffer(Math.ceil(s)); // NOT OK [INCONSISTENCY]
|
||||
new Buffer(Number(s)); // NOT OK [INCONSISTENCY]
|
||||
new Buffer(new Number(s)); // OK
|
||||
|
||||
new Buffer(s + x.length); // OK (this is a string if `s` is a string)
|
||||
new Buffer(s.length); // NOT OK [INCONSISTENCY]
|
||||
|
||||
if (n < 100) {
|
||||
new Buffer(n); // OK
|
||||
} else {
|
||||
new Buffer(n); // NOT OK [INCONSISTENCY]
|
||||
}
|
||||
|
||||
let ns = x ? n : s;
|
||||
new Buffer(ns); // NOT OK [INCONSISTENCY]
|
||||
|
||||
new Buffer(n.toString()); // OK
|
||||
|
||||
if (typeof n === "string") {
|
||||
new Buffer(n); // OK
|
||||
} else {
|
||||
new Buffer(n); // NOT OK [INCONSISTENCY]
|
||||
}
|
||||
|
||||
if (typeof n === "number") {
|
||||
new Buffer(n); // NOT OK [INCONSISTENCY]
|
||||
} else {
|
||||
new Buffer(n); // OK
|
||||
}
|
||||
|
||||
if (typeof s === "number") {
|
||||
new Buffer(s); // NOT OK [INCONSISTENCY]
|
||||
} else {
|
||||
new Buffer(s); // OK
|
||||
}
|
||||
|
||||
setTimeout(f, n); // NOT OK
|
||||
setTimeout(f, s); // NOT OK
|
||||
setInterval(f, n); // NOT OK
|
||||
setInterval(f, s); // NOT OK
|
||||
});
|
||||
Reference in New Issue
Block a user