Skip to content

Commit ff36d19

Browse files
authored
Merge pull request #18810 from asgerf/js/test-related-locations
Test: Add support for RelatedLocation tag and use in a JS query
2 parents 11d1451 + bb8f452 commit ff36d19

22 files changed

+207
-88
lines changed

csharp/ql/test/utils/inline-tests/InlineTests.cs

+21-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ void Problems()
1616

1717
// correct expectation comment
1818
x = "Alert"; // $ Alert[problem-query]
19+
20+
// correct expectation comments with a related location
21+
var related = "Related"; // $ RelatedLocation[problem-query-with-related-loc]
22+
x = "Alert:1"; // $ Alert[problem-query-with-related-loc]
23+
24+
// expectation comments missing the related location
25+
related = "Related";
26+
x = "Alert:1"; // $ Alert[problem-query-with-related-loc]
1927
}
2028

2129
void PathProblems()
@@ -78,5 +86,17 @@ void PathProblems()
7886
// incorrect expectation comments, using an identifier tag; the alert location coincides with the source location
7987
sink = "Sink"; // $ Sink[path-problem-query]=sink2
8088
x = "Alert:0:1"; // $ Alert[path-problem-query]=sink1
89+
90+
// correct expectation comments with a related location
91+
source = "Source"; // $ Source[path-problem-query-with-related-loc]
92+
sink = "Sink"; // $ Sink[path-problem-query-with-related-loc]
93+
var related = "Related"; // $ RelatedLocation[path-problem-query-with-related-loc]
94+
x = "Alert:3:2:1"; // $ Alert[path-problem-query-with-related-loc]
95+
96+
// expectation comments missing the related location
97+
source = "Source"; // $ Source[path-problem-query-with-related-loc]
98+
sink = "Sink"; // $ Sink[path-problem-query-with-related-loc]
99+
related = "Related";
100+
x = "Alert:3:2:1"; // $ Alert[path-problem-query-with-related-loc]
81101
}
82-
}
102+
}
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
#select
2-
| InlineTests.cs:26:17:26:27 | "Alert:2:1" | InlineTests.cs:24:22:24:29 | "Source" | InlineTests.cs:25:20:25:25 | "Sink" | This is a problem |
3-
| InlineTests.cs:36:13:36:23 | "Alert:2:1" | InlineTests.cs:34:18:34:25 | "Source" | InlineTests.cs:35:16:35:21 | "Sink" | This is a problem |
4-
| InlineTests.cs:41:13:41:23 | "Alert:2:1" | InlineTests.cs:39:18:39:25 | "Source" | InlineTests.cs:40:16:40:21 | "Sink" | This is a problem |
5-
| InlineTests.cs:45:13:45:23 | "Alert:1:0" | InlineTests.cs:44:18:44:25 | "Source" | InlineTests.cs:45:13:45:23 | "Alert:1:0" | This is a problem |
6-
| InlineTests.cs:49:13:49:23 | "Alert:0:1" | InlineTests.cs:49:13:49:23 | "Alert:0:1" | InlineTests.cs:48:16:48:21 | "Sink" | This is a problem |
7-
| InlineTests.cs:54:13:54:23 | "Alert:2:1" | InlineTests.cs:52:18:52:25 | "Source" | InlineTests.cs:53:16:53:21 | "Sink" | This is a problem |
8-
| InlineTests.cs:59:13:59:23 | "Alert:2:1" | InlineTests.cs:57:18:57:25 | "Source" | InlineTests.cs:58:16:58:21 | "Sink" | This is a problem |
9-
| InlineTests.cs:64:13:64:23 | "Alert:2:1" | InlineTests.cs:62:18:62:25 | "Source" | InlineTests.cs:63:16:63:21 | "Sink" | This is a problem |
10-
| InlineTests.cs:68:13:68:23 | "Alert:1:0" | InlineTests.cs:67:18:67:25 | "Source" | InlineTests.cs:68:13:68:23 | "Alert:1:0" | This is a problem |
11-
| InlineTests.cs:72:13:72:23 | "Alert:1:0" | InlineTests.cs:71:18:71:25 | "Source" | InlineTests.cs:72:13:72:23 | "Alert:1:0" | This is a problem |
12-
| InlineTests.cs:76:13:76:23 | "Alert:0:1" | InlineTests.cs:76:13:76:23 | "Alert:0:1" | InlineTests.cs:75:16:75:21 | "Sink" | This is a problem |
13-
| InlineTests.cs:80:13:80:23 | "Alert:0:1" | InlineTests.cs:80:13:80:23 | "Alert:0:1" | InlineTests.cs:79:16:79:21 | "Sink" | This is a problem |
2+
| InlineTests.cs:34:17:34:27 | "Alert:2:1" | InlineTests.cs:32:22:32:29 | "Source" | InlineTests.cs:33:20:33:25 | "Sink" | This is a problem |
3+
| InlineTests.cs:44:13:44:23 | "Alert:2:1" | InlineTests.cs:42:18:42:25 | "Source" | InlineTests.cs:43:16:43:21 | "Sink" | This is a problem |
4+
| InlineTests.cs:49:13:49:23 | "Alert:2:1" | InlineTests.cs:47:18:47:25 | "Source" | InlineTests.cs:48:16:48:21 | "Sink" | This is a problem |
5+
| InlineTests.cs:53:13:53:23 | "Alert:1:0" | InlineTests.cs:52:18:52:25 | "Source" | InlineTests.cs:53:13:53:23 | "Alert:1:0" | This is a problem |
6+
| InlineTests.cs:57:13:57:23 | "Alert:0:1" | InlineTests.cs:57:13:57:23 | "Alert:0:1" | InlineTests.cs:56:16:56:21 | "Sink" | This is a problem |
7+
| InlineTests.cs:62:13:62:23 | "Alert:2:1" | InlineTests.cs:60:18:60:25 | "Source" | InlineTests.cs:61:16:61:21 | "Sink" | This is a problem |
8+
| InlineTests.cs:67:13:67:23 | "Alert:2:1" | InlineTests.cs:65:18:65:25 | "Source" | InlineTests.cs:66:16:66:21 | "Sink" | This is a problem |
9+
| InlineTests.cs:72:13:72:23 | "Alert:2:1" | InlineTests.cs:70:18:70:25 | "Source" | InlineTests.cs:71:16:71:21 | "Sink" | This is a problem |
10+
| InlineTests.cs:76:13:76:23 | "Alert:1:0" | InlineTests.cs:75:18:75:25 | "Source" | InlineTests.cs:76:13:76:23 | "Alert:1:0" | This is a problem |
11+
| InlineTests.cs:80:13:80:23 | "Alert:1:0" | InlineTests.cs:79:18:79:25 | "Source" | InlineTests.cs:80:13:80:23 | "Alert:1:0" | This is a problem |
12+
| InlineTests.cs:84:13:84:23 | "Alert:0:1" | InlineTests.cs:84:13:84:23 | "Alert:0:1" | InlineTests.cs:83:16:83:21 | "Sink" | This is a problem |
13+
| InlineTests.cs:88:13:88:23 | "Alert:0:1" | InlineTests.cs:88:13:88:23 | "Alert:0:1" | InlineTests.cs:87:16:87:21 | "Sink" | This is a problem |
1414
edges
1515
testFailures
1616
| InlineTests.cs:6:26:6:35 | // ... | Missing result: Alert |
1717
| InlineTests.cs:12:34:12:43 | // ... | Missing result: Alert |
18-
| InlineTests.cs:29:28:29:38 | // ... | Missing result: Source |
19-
| InlineTests.cs:30:24:30:32 | // ... | Missing result: Sink |
20-
| InlineTests.cs:31:33:31:42 | // ... | Missing result: Alert |
21-
| InlineTests.cs:34:18:34:25 | "Source" | Unexpected result: Source |
22-
| InlineTests.cs:35:16:35:21 | "Sink" | Unexpected result: Sink |
23-
| InlineTests.cs:36:13:36:23 | InlineTests.cs:34:18:34:25 | Unexpected result: Alert |
24-
| InlineTests.cs:58:16:58:21 | "Sink" | Unexpected result: Sink=source2 |
25-
| InlineTests.cs:58:24:58:60 | // ... | Missing result: Sink[path-problem-query]=source1 |
26-
| InlineTests.cs:64:13:64:23 | InlineTests.cs:62:18:62:25 | Unexpected result: Alert=source3 |
27-
| InlineTests.cs:64:26:64:63 | // ... | Missing result: Alert[path-problem-query]=source2 |
28-
| InlineTests.cs:72:13:72:23 | "Alert:1:0" | Unexpected result: Alert=source5 |
29-
| InlineTests.cs:72:26:72:63 | // ... | Missing result: Alert[path-problem-query]=source4 |
30-
| InlineTests.cs:79:16:79:21 | "Sink" | Unexpected result: Sink=sink1 |
31-
| InlineTests.cs:79:24:79:58 | // ... | Missing result: Sink[path-problem-query]=sink2 |
18+
| InlineTests.cs:37:28:37:38 | // ... | Missing result: Source |
19+
| InlineTests.cs:38:24:38:32 | // ... | Missing result: Sink |
20+
| InlineTests.cs:39:33:39:42 | // ... | Missing result: Alert |
21+
| InlineTests.cs:42:18:42:25 | "Source" | Unexpected result: Source |
22+
| InlineTests.cs:43:16:43:21 | "Sink" | Unexpected result: Sink |
23+
| InlineTests.cs:44:13:44:23 | InlineTests.cs:42:18:42:25 | Unexpected result: Alert |
24+
| InlineTests.cs:66:16:66:21 | "Sink" | Unexpected result: Sink=source2 |
25+
| InlineTests.cs:66:24:66:60 | // ... | Missing result: Sink[path-problem-query]=source1 |
26+
| InlineTests.cs:72:13:72:23 | InlineTests.cs:70:18:70:25 | Unexpected result: Alert=source3 |
27+
| InlineTests.cs:72:26:72:63 | // ... | Missing result: Alert[path-problem-query]=source2 |
28+
| InlineTests.cs:80:13:80:23 | "Alert:1:0" | Unexpected result: Alert=source5 |
29+
| InlineTests.cs:80:26:80:63 | // ... | Missing result: Alert[path-problem-query]=source4 |
30+
| InlineTests.cs:87:16:87:21 | "Sink" | Unexpected result: Sink=sink1 |
31+
| InlineTests.cs:87:24:87:58 | // ... | Missing result: Sink[path-problem-query]=sink2 |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#select
2+
| InlineTests.cs:94:13:94:25 | "Alert:3:2:1" | InlineTests.cs:91:18:91:25 | "Source" | InlineTests.cs:92:16:92:21 | "Sink" | This is a problem with $@ | InlineTests.cs:93:23:93:31 | "Related" | a related location |
3+
| InlineTests.cs:100:13:100:25 | "Alert:3:2:1" | InlineTests.cs:97:18:97:25 | "Source" | InlineTests.cs:98:16:98:21 | "Sink" | This is a problem with $@ | InlineTests.cs:99:19:99:27 | "Related" | a related location |
4+
edges
5+
testFailures
6+
| InlineTests.cs:6:26:6:35 | // ... | Missing result: Alert |
7+
| InlineTests.cs:12:34:12:43 | // ... | Missing result: Alert |
8+
| InlineTests.cs:32:32:32:42 | // ... | Missing result: Source |
9+
| InlineTests.cs:33:28:33:36 | // ... | Missing result: Sink |
10+
| InlineTests.cs:34:30:34:39 | // ... | Missing result: Alert |
11+
| InlineTests.cs:37:28:37:38 | // ... | Missing result: Source |
12+
| InlineTests.cs:38:24:38:32 | // ... | Missing result: Sink |
13+
| InlineTests.cs:39:33:39:42 | // ... | Missing result: Alert |
14+
| InlineTests.cs:99:19:99:27 | "Related" | Unexpected result: RelatedLocation |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
query: utils/inline-tests/queries/PathProblemQueryRelatedLocs.ql
2+
postprocess: utils/test/InlineExpectationsTestQuery.ql

csharp/ql/test/utils/inline-tests/ProblemQuery.expected

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
testFailures
66
| InlineTests.cs:12:34:12:43 | // ... | Missing result: Alert |
77
| InlineTests.cs:15:13:15:19 | This is a problem | Unexpected result: Alert |
8-
| InlineTests.cs:26:30:26:39 | // ... | Missing result: Alert |
9-
| InlineTests.cs:31:33:31:42 | // ... | Missing result: Alert |
8+
| InlineTests.cs:34:30:34:39 | // ... | Missing result: Alert |
9+
| InlineTests.cs:39:33:39:42 | // ... | Missing result: Alert |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#select
2+
| InlineTests.cs:22:13:22:21 | "Alert:1" | This is a problem with $@ | InlineTests.cs:21:23:21:31 | "Related" | a related location |
3+
| InlineTests.cs:26:13:26:21 | "Alert:1" | This is a problem with $@ | InlineTests.cs:25:19:25:27 | "Related" | a related location |
4+
testFailures
5+
| InlineTests.cs:6:26:6:35 | // ... | Missing result: Alert |
6+
| InlineTests.cs:12:34:12:43 | // ... | Missing result: Alert |
7+
| InlineTests.cs:25:19:25:27 | "Related" | Unexpected result: RelatedLocation |
8+
| InlineTests.cs:34:30:34:39 | // ... | Missing result: Alert |
9+
| InlineTests.cs:39:33:39:42 | // ... | Missing result: Alert |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
query: utils/inline-tests/queries/ProblemQueryRelatedLocs.ql
2+
postprocess: utils/test/InlineExpectationsTestQuery.ql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
edges
2+
#select
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @kind path-problem
3+
* @id path-problem-query-with-related-loc
4+
*/
5+
6+
import csharp
7+
8+
query predicate edges(StringLiteral sl1, StringLiteral sl2) { none() }
9+
10+
from StringLiteral alert, StringLiteral source, StringLiteral sink, StringLiteral related
11+
where
12+
exists(string regexp, int sourceOffset, int sinkOffset, int relatedOffset |
13+
regexp = "Alert:([0-9]+):([0-9]+):([0-9]+)"
14+
|
15+
sourceOffset = alert.getValue().regexpCapture(regexp, 1).toInt() and
16+
sinkOffset = alert.getValue().regexpCapture(regexp, 2).toInt() and
17+
relatedOffset = alert.getValue().regexpCapture(regexp, 3).toInt() and
18+
source.getLocation().getStartLine() = alert.getLocation().getStartLine() - sourceOffset and
19+
sink.getLocation().getStartLine() = alert.getLocation().getStartLine() - sinkOffset and
20+
related.getLocation().getStartLine() = alert.getLocation().getStartLine() - relatedOffset
21+
)
22+
select alert, source, sink, "This is a problem with $@", related, "a related location"

csharp/ql/test/utils/inline-tests/queries/ProblemQueryRelatedLocs.expected

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @kind problem
3+
* @id problem-query-with-related-loc
4+
*/
5+
6+
import csharp
7+
8+
from StringLiteral sl, StringLiteral related, int offset
9+
where
10+
sl.getValue().regexpCapture("Alert:([0-9]+)", 1).toInt() = offset and
11+
related.getLocation().getStartLine() = sl.getLocation().getStartLine() - offset
12+
select sl, "This is a problem with $@", related, "a related location"
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
Security/CWE-352/MissingCsrfMiddleware.ql
1+
query: Security/CWE-352/MissingCsrfMiddleware.ql
2+
postprocess: utils/test/InlineExpectationsTestQuery.ql

javascript/ql/test/query-tests/Security/CWE-352/MissingCsrfMiddlewareBad.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ var passport = require('passport');
44

55
var app = express();
66

7-
app.use(cookieParser());
7+
app.use(cookieParser()); // $ Alert
88
app.use(passport.authorize({ session: true }));
99

1010
app.post('/changeEmail', function (req, res) {
1111
let newEmail = req.cookies["newEmail"];
12-
});
12+
}); // $ RelatedLocation
1313

1414
(function () {
1515
var app = express();
1616

17-
app.use(cookieParser());
17+
app.use(cookieParser()); // $ Alert
1818
app.use(passport.authorize({ session: true }));
1919

2020
const errorCatch = (fn) =>
@@ -24,13 +24,13 @@ app.post('/changeEmail', function (req, res) {
2424

2525
app.post('/changeEmail', errorCatch(async function (req, res) {
2626
let newEmail = req.cookies["newEmail"];
27-
}));
27+
})); // $ RelatedLocation
2828
})
2929

3030
(function () {
3131
var app = express();
3232

33-
app.use(cookieParser());
33+
app.use(cookieParser()); // $ Alert
3434
app.use(passport.authorize({ session: true }));
3535

3636
const errorCatch = (fn) =>
@@ -40,9 +40,9 @@ app.post('/changeEmail', function (req, res) {
4040

4141
app.post('/changeEmail', errorCatch(async function (req, res) {
4242
let newEmail = req.cookies["newEmail"];
43-
}));
43+
})); // $ RelatedLocation
4444

4545
app.post('/doLoginStuff', errorCatch(async function (req, res) {
4646
req.session.user = loginStuff(req);
47-
}));
47+
})); // $ RelatedLocation
4848
})

javascript/ql/test/query-tests/Security/CWE-352/csurf_api_example.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ function createApiRouter () {
3939
res.send('no csrf to get here')
4040
})
4141

42-
router.post('/getProfile_unsafe', cookieParser(), function (req, res) { // NOT OK - may use cookies
42+
router.post('/getProfile_unsafe', cookieParser(), function (req, res) { // $ Alert - may use cookies
4343
let newEmail = req.cookies["newEmail"];
4444
res.send('no csrf to get here')
45-
})
45+
}) // $ RelatedLocation
4646

4747
return router
4848
}

javascript/ql/test/query-tests/Security/CWE-352/csurf_example.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var app = express()
1515

1616
// parse cookies
1717
// we need this because "cookie" is true in csrfProtection
18-
app.use(cookieParser())
18+
app.use(cookieParser()) // $ Alert
1919

2020
app.get('/form', csrfProtection, function (req, res) { // OK
2121
let newEmail = req.cookies["newEmail"];
@@ -28,7 +28,7 @@ app.post('/process', parseForm, csrfProtection, function (req, res) { // OK
2828
res.send('data is being processed')
2929
})
3030

31-
app.post('/process_unsafe', parseForm, function (req, res) { // NOT OK
31+
app.post('/process_unsafe', parseForm, function (req, res) {
3232
let newEmail = req.cookies["newEmail"];
3333
res.send('data is being processed')
34-
})
34+
}) // $ RelatedLocation

javascript/ql/test/query-tests/Security/CWE-352/fastify.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const fastify = require('fastify')
22

33
const app = fastify();
44

5-
app.register(require('fastify-cookie'));
5+
app.register(require('fastify-cookie')); // $ Alert
66
app.register(require('fastify-csrf'));
77

88
app.route({
@@ -17,10 +17,10 @@ app.route({
1717
app.route({
1818
method: 'POST',
1919
path: '/',
20-
handler: async (req, reply) => { // NOT OK - lacks CSRF protection
20+
handler: async (req, reply) => { // lacks CSRF protection
2121
req.session.blah;
2222
return req.body
23-
}
23+
} // $ RelatedLocation
2424
})
2525

2626

javascript/ql/test/query-tests/Security/CWE-352/fastify2.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const fp = require('fastify-plugin');
44
const app = fastify();
55

66
function plugin(app) {
7-
app.register(require('fastify-cookie'));
7+
app.register(require('fastify-cookie')); // $ Alert
88
app.register(require('fastify-csrf'));
99
}
1010
app.register(fp(plugin));
@@ -21,10 +21,10 @@ app.route({
2121
app.route({
2222
method: 'POST',
2323
path: '/',
24-
handler: async (req, reply) => { // NOT OK - lacks CSRF protection
24+
handler: async (req, reply) => { // lacks CSRF protection
2525
req.session.blah;
2626
return req.body
27-
}
27+
} // $ RelatedLocation
2828
})
2929

3030

javascript/ql/test/query-tests/Security/CWE-352/lusca_example.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ var parseForm = bodyParser.urlencoded({ extended: false })
66
var lusca = require('lusca');
77

88
var app = express()
9-
app.use(cookieParser())
9+
app.use(cookieParser()) // $ Alert
1010

1111
app.post('/process', parseForm, lusca.csrf(), function (req, res) { // OK
1212
let newEmail = req.cookies["newEmail"];
@@ -23,12 +23,12 @@ app.post('/process', parseForm, lusca({csrf:{}}), function (req, res) { // OK
2323
res.send('data is being processed')
2424
})
2525

26-
app.post('/process', parseForm, lusca(), function (req, res) { // NOT OK - missing csrf option
26+
app.post('/process', parseForm, lusca(), function (req, res) { // missing csrf option
2727
let newEmail = req.cookies["newEmail"];
2828
res.send('data is being processed')
29-
})
29+
}) // $ RelatedLocation
3030

31-
app.post('/process_unsafe', parseForm, function (req, res) { // NOT OK
31+
app.post('/process_unsafe', parseForm, function (req, res) {
3232
let newEmail = req.cookies["newEmail"];
3333
res.send('data is being processed')
34-
})
34+
}) // $ RelatedLocation

javascript/ql/test/query-tests/Security/CWE-352/tst.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ const cookieParser = require('cookie-parser')
33
const csrf = require('csurf')
44

55
const app = express()
6-
app.use(cookieParser())
6+
app.use(cookieParser()) // $ Alert
77

8-
app.post('/unsafe', (req, res) => { // NOT OK
8+
app.post('/unsafe', (req, res) => {
99
req.cookies.x;
10-
});
10+
}); // $ RelatedLocation
1111

1212
function middlewares() {
1313
return express.Router()

javascript/ql/test/query-tests/Security/CWE-352/unused_cookies.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ let cookieParser = require('cookie-parser');
33

44
let app = express();
55

6-
app.use(cookieParser());
6+
app.use(cookieParser()); // $ Alert
77

8-
app.post('/doSomethingTerrible', (req, res) => { // NOT OK - uses cookies
8+
app.post('/doSomethingTerrible', (req, res) => { // uses cookies
99
if (req.cookies['secret'] === app.secret) {
1010
somethingTerrible();
1111
}
1212
res.end('Ok');
13-
});
13+
}); // $ RelatedLocation
1414

1515
app.post('/doSomethingElse', (req, res) => { // OK - doesn't actually use cookies
1616
somethingElse(req.query['data']);
@@ -26,14 +26,14 @@ app.post('/doWithCaptcha', (req, res) => { // OK - attacker can't guess the capt
2626
res.end('Ok');
2727
});
2828

29-
app.post('/user', (req, res) => { // NOT OK - access to req.user is unprotected
29+
app.post('/user', (req, res) => { // access to req.user is unprotected
3030
somethingElse(req.user.name);
3131
res.end('Ok');
32-
});
32+
}); // $ RelatedLocation
3333

34-
app.post('/session', (req, res) => { // NOT OK - access to req.session is unprotected
34+
app.post('/session', (req, res) => { // access to req.session is unprotected
3535
somethingElse(req.session.name);
3636
res.end('Ok');
37-
});
37+
}); // $ RelatedLocation
3838

3939
app.listen();

0 commit comments

Comments
 (0)