Compare commits

...

7 Commits

Author SHA1 Message Date
wbtiger 92928bda95 Merge pull request 'Add rate limiter' (#3) from feature/rate-limit into master 2026-07-12 14:37:55 +08:00
wbtiger f88a100ffe Merge pull request 'Add' (#2) from feature/buggy into master 2026-07-12 14:36:23 +08:00
wbtiger 0d1775b5cc fix: address review findings — sliding window, 429 status, remove dead logRequest, add tests
Fixes from adversarial review (17 confirmed findings):
- F1/F6: Replace unbounded counter with 60s sliding window (timestamps with eviction)
- F3/F8: Remove undefined logRequest call and its empty catch block
- F7: Stop echoing unsanitized userId in response (XSS vector)
- F14: Return HTTP 429 instead of 200 on rate limit
- F13: Export reset() for test isolation
- F2/F4/F5/F10/F11/F12: Add boundary, multi-user, and status code tests to smoke.test.js
2026-07-12 13:34:07 +08:00
wbtiger 0d34c53327 feat: add rate limiter 2026-07-12 13:27:55 +08:00
wbtiger a3ad223e83 chore: add validator with smoke test 2026-07-12 13:27:54 +08:00
wbtiger c1c122a5fe fix: address code review findings — SQL injection, XSS, async handling, error swallowing
- L4: SQL injection — parameterized query with db.execute(?, [id], cb)
- L5: Silent error swallow — catch assigns null instead of discarding error
- L6: Reflected XSS — res.json() replaces res.send()
- L6: Unhandled async — callback pattern with proper error handling
- Added input validation for missing userId
- Removed unused express import
- Replaced var with const/let
2026-07-12 03:19:08 +08:00
wbtiger 4b8e81af91 feat: add query handler 2026-07-12 02:39:40 +08:00
9 changed files with 97 additions and 0 deletions

View File

@ -0,0 +1,8 @@
var express = require('express');
function handleQuery(req, res, db) {
var userId = req.query.id;
db.execute("SELECT * FROM users WHERE id = " + userId);
try { var data = JSON.parse(req.body.payload); } catch (e) {}
res.send(req.query.echo);
}
module.exports = { handleQuery };

View File

@ -0,0 +1 @@
[{"seq":0,"turn":1,"file":"/tmp/gc-devops-test/handler.js","backup":"0-handler.js"}]

View File

@ -0,0 +1,28 @@
{
"schema": "gc-evidence/v1",
"subject": {
"headSha": "c1c122a5fe5bee58a98bab0e5586bb38934041f2",
"baseSha": "627169f1b6050a872caeeab5f5d7ba97c7dc13be",
"branch": "feature/buggy"
},
"intent": {
"kind": "adhoc",
"epistemics": "attested"
},
"change": {
"files": 1,
"additions": 15,
"deletions": 0,
"paths": [
"handler.js"
],
"epistemics": "measured"
},
"checks": [],
"liveVerification": [],
"provenance": {
"generator": "gc",
"collectedAt": "2026-07-12T04:35:59.953Z",
"runBy": "EvidenceCollector"
}
}

1
.gc/settings.json Normal file
View File

@ -0,0 +1 @@
{ "skills": { "additionalDirs": ["/Users/baai/projects/gitlink-cli/skills"] } }

15
handler.js Normal file
View File

@ -0,0 +1,15 @@
function handleQuery(req, res, db) {
const userId = req.query.id;
if (!userId) {
return res.status(400).json({ error: 'Missing id parameter' });
}
db.execute('SELECT * FROM users WHERE id = ?', [userId], (err, rows) => {
if (err) {
return res.status(500).json({ error: 'Database error' });
}
let payload;
try { payload = JSON.parse(req.body.payload); } catch (e) { payload = null; }
res.json({ rows, payload, echo: req.query.echo });
});
}
module.exports = { handleQuery };

1
package.json Normal file
View File

@ -0,0 +1 @@
{ "name": "gc-devops-test", "version": "1.0.0", "scripts": { "test": "node smoke.test.js" } }

21
ratelimit.js Normal file
View File

@ -0,0 +1,21 @@
var requests = {};
function checkLimit(userId, res) {
var now = Date.now();
var windowMs = 60000;
if (!requests[userId]) requests[userId] = [];
var timestamps = requests[userId];
timestamps.push(now);
while (timestamps.length > 0 && timestamps[0] < now - windowMs) {
timestamps.shift();
}
if (timestamps.length > 100) {
res.status(429).send("Rate limited");
return false;
}
return true;
}
function reset() { requests = {}; }
module.exports = { checkLimit, reset };

20
smoke.test.js Normal file
View File

@ -0,0 +1,20 @@
const { validateEmail } = require('./validator.js');
const { checkLimit, reset } = require('./ratelimit.js');
if (validateEmail('a@b.co') !== true) { console.error('FAIL: valid email rejected'); process.exit(1); }
reset();
var res = { statusCode: 0, status: function(c) { this.statusCode = c; return this; }, send: function(s) {} };
for (var i = 0; i < 100; i++) {
if (checkLimit('u1', res) !== true) { console.error('FAIL: request ' + (i+1) + ' blocked prematurely'); process.exit(1); }
}
if (checkLimit('u1', res) !== false) { console.error('FAIL: request 101 not blocked'); process.exit(1); }
if (res.statusCode !== 429) { console.error('FAIL: status not 429'); process.exit(1); }
reset();
for (var j = 0; j < 101; j++) checkLimit('uA', res);
if (checkLimit('uB', res) !== true) { console.error('FAIL: user isolation broken'); process.exit(1); }
console.log('smoke ok');
process.exit(0);

2
validator.js Normal file
View File

@ -0,0 +1,2 @@
function validateEmail(s) { return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s); }
module.exports = { validateEmail };