Severity
- Severity: Low–Medium
- CVSS v3.1 score: 5.3 (indicative)
- CVSS v3.1 vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
- CWE: CWE-770 — Allocation of Resources Without Limits or Throttling
CVSS assumptions and stated limitation: A:L is scored because unbounded response size and unthrottled
unauthenticated writes demonstrably consume disproportionate server/bandwidth resources per request. No denial
of service was demonstrated in this audit — no timeout, crash, or service outage was produced — so A:H is
deliberately not claimed. If a service-availability loss were later demonstrated, the vector would become
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 7.5. Integrity impact is scored as none here because the
ability to write anonymously is reported separately under the missing-authorization finding.
Summary
The read path issues SELECT * FROM tutorials with no LIMIT and no pagination parameters, so a single
request returns every row in the table. With 20,008 rows in the test table, one unauthenticated request
returned a 1.5 MB response in 33 ms, and a filtered variant returned 1.54 MB in 29 ms — meaning an attacker can
make the application generate large payloads cheaply and repeatedly.
There is no rate limiting and no authentication anywhere in the application, so the same caller can also grow
the table without bound: 50 anonymous POST requests increased the row count from 20,008 to 20,058 with no
quota, credential or throttle.
One part of the request-side picture is bounded and is recorded accurately: a 300 KB request body was rejected
with HTTP 413 by Express's default 100 kB limit. The gap is therefore in result size, write volume and
request rate, not in single-request body size.
Affected Component
| Item |
Value |
| Repository |
bezkoder/nodejs-express-mysql |
| Affected files |
app/models/tutorial.model.js getAll() (no LIMIT/pagination in the query), app/routes/tutorial.routes.js (no throttling middleware), server.js (no rate-limit middleware) |
| Affected endpoints |
GET /api/tutorials, GET /api/tutorials?title=, POST /api/tutorials, DELETE /api/tutorials |
| Affected commit |
68d3959 |
Technical Description
// app/models/tutorial.model.js — getAll()
let query = "SELECT * FROM tutorials";
if (title) query += ` WHERE title LIKE '%${title}%'`;
sql.query(query, (err, res) => …); // no LIMIT / OFFSET, no row cap, no streaming
There is no limit/offset query parameter, no maximum result size, no per-account quota, and no rate-limit
middleware in server.js or the router. Because no authentication exists, all four endpoints above are
reachable anonymously, so both the consumption side (large reads) and the growth side (writes) are open to any
caller.
Data flow:
GET /api/tutorials (unauthenticated)
→ findAll (tutorial.controller.js:31)
→ Tutorial.getAll (no LIMIT)
→ SELECT * FROM tutorials
→ every row serialized into one JSON response (1,538,419 bytes observed)
Attack Preconditions
- Authentication required: none — reads and writes are anonymous.
- Network access: HTTP access to the API port.
- Privileges required: none.
- Special configuration required: none; the absence of pagination is in the source.
- Works against a default installation: yes; the response size scales with how many rows exist (the table
must first be filled — achievable anonymously, as shown below, or simply by normal application use).
- Assumptions used in the reproduction: isolated local MariaDB; the table was seeded to 20,000 rows of
harmless generated text to measure response size.
Step-by-Step Reproduction / PoC
PoC 1 — Unbounded read (least destructive version: one request, no writes)
# with 20,008 rows present
curl -s -o /tmp/out.json -w 'status=%{http_code} bytes=%{size_download} time=%{time_total}s\n' \
http://localhost:8080/api/tutorials
curl -s -o /tmp/out2.json -w 'status=%{http_code} bytes=%{size_download} time=%{time_total}s\n' \
'http://localhost:8080/api/tutorials?title=bulk'
PoC 2 — Unauthenticated, unthrottled writes (small volume, bounded on purpose)
for i in $(seq 1 50); do
curl -s -o /dev/null -w '%{http_code} ' -X POST http://localhost:8080/api/tutorials \
-H 'Content-Type: application/json' \
-d "{\"title\":\"quota-$i\",\"description\":\"anonymous write\",\"published\":false}"
done
PoC 3 — Control: request body size is bounded (not a finding)
# 300 KB JSON body
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8080/api/tutorials \
-H 'Content-Type: application/json' --data-binary @/tmp/300kb.json
Observed Result
PoC 1 (/tmp/opencode/audit-lab-FINAL.log, section 10):
########## 10. UNBOUNDED RESULT SET (no pagination, no LIMIT) ##########
inserted 20000 rows
rows=20008 response=1538419 bytes time=33 ms (single request, no ?limit)
?title=bulk -> rows returned in 29 ms, 1537840 bytes
PoC 2:
-- 10b. unauthenticated writes -> unbounded storage growth (no auth, no quota)
50 anonymous POSTs: rows 20008 -> 20058 (no credentials, no rate limit, no quota)
PoC 3 (control):
-- 7d. request body size limit (express default 100kb)
POST /api/tutorials -> HTTP 413
No service interruption, timeout or crash was produced by these tests; the demonstrated condition is
resource exhaustion risk / unbounded consumption, not a confirmed denial of service.
Expected Secure Result
- Every list endpoint paginates with a server-enforced maximum page size (for example default 20, cap 100).
- Writes require authentication and are subject to per-account quotas and rate limits.
- Repeated requests above a threshold are throttled (
429 Too Many Requests).
- Large or expensive queries are bounded (hard
LIMIT, statement timeout) and expensive endpoints are
monitored.
Security Impact
Direct impact
An unauthenticated caller can force the application to serialize and transmit arbitrarily large responses (1.5 MB
per request observed at 20k rows, scaling linearly with table size) and can grow storage without limit via
anonymous inserts, increasing disk, CPU, memory and bandwidth consumption per request. Repeated requests amplify
that cost on both the application and the network path.
Amplified impact
- Root cause of this finding: absent pagination, result caps, quotas and rate limiting (this file).
- Interaction with other findings (not causes): the missing-authorization finding is what makes writes
anonymous, and the SQL-injection finding's table reads are unaffected by pagination. Fixing this finding
bounds resource use but does not remove anonymous access; fixing authorization does not bound result size.
- Not claimed: denial of service, service outage, or crash — none were demonstrated.
Evidence
- Source:
app/models/tutorial.model.js getAll() (no LIMIT), app/routes/tutorial.routes.js,
server.js (no rate-limit middleware) at commit 68d3959.
- Measured response sizes and timings:
/tmp/opencode/audit-lab-FINAL.log, section 10 (1,538,419 bytes /
33 ms; 1,537,840 bytes / 29 ms).
- Anonymous write growth: section 10b (20008 → 20058 rows over 50 requests).
- Body-size control: section 7d (
HTTP 413).
- Harness:
/tmp/opencode/audit-lab.sh.
- Post-fix status: still open (outside the scope of the applied code patch).
Root Cause
The query layer returns complete result sets with no upper bound, and no middleware enforces request rate,
request count or write quota, so per-request resource cost and total resource growth are both unconstrained and
reachable without credentials.
Remediation
// app/controllers/tutorial.controller.js — bounded pagination
exports.findAll = (req, res) => {
const title = req.query.title;
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 20, 1), 100);
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0);
Tutorial.getAll(title, limit, offset, (err, data) => {
if (err) {
console.error(err);
return res.status(500).send({ message: "Some error occurred while retrieving tutorials." });
}
res.send(data);
});
};
// app/models/tutorial.model.js — bound values, bounded result
let query = "SELECT * FROM tutorials";
const params = [];
if (title) { query += " WHERE title LIKE ?"; params.push("%" + title + "%"); }
query += " LIMIT ? OFFSET ?";
params.push(limit, offset);
sql.query(query, params, callback);
// server.js — throttle write routes
const rateLimit = require("express-rate-limit");
app.use("/api/tutorials", rateLimit({
windowMs: 60 * 1000,
limit: 60,
standardHeaders: "draft-7",
legacyHeaders: false
}));
Pair with authentication and a per-account creation quota so storage growth is capped per identity, and return
count/total metadata so clients can page without fetching everything.
Defense in Depth
- Statement timeouts / connection-pool limits to bound expensive queries.
- Cache or materialize
GET /api/tutorials/published-style reads.
- Monitor response-size and query-duration percentiles; alert on outliers.
- Cap
DELETE /api/tutorials behind an administrative role so bulk operations cannot be triggered anonymously.
- Keep the existing request-body limit (100 kB, verified as
HTTP 413) — it is doing its job.
Affected Versions / Git History
getAll() has existed without a LIMIT since app/models/tutorial.model.js was created in commit
be702f0 (2021-11-10); no rate-limiting or pagination middleware has ever been added to server.js, which
was last changed by that same commit.
- Present in current
master, commit 68d3959 (2024-02-04). No release tags exist, so the affected version is
described by commit.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LCVSS assumptions and stated limitation: A:L is scored because unbounded response size and unthrottled
unauthenticated writes demonstrably consume disproportionate server/bandwidth resources per request. No denial
of service was demonstrated in this audit — no timeout, crash, or service outage was produced — so
A:Hisdeliberately not claimed. If a service-availability loss were later demonstrated, the vector would become
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H= 7.5. Integrity impact is scored as none here because theability to write anonymously is reported separately under the missing-authorization finding.
Summary
The read path issues
SELECT * FROM tutorialswith noLIMITand no pagination parameters, so a singlerequest returns every row in the table. With 20,008 rows in the test table, one unauthenticated request
returned a 1.5 MB response in 33 ms, and a filtered variant returned 1.54 MB in 29 ms — meaning an attacker can
make the application generate large payloads cheaply and repeatedly.
There is no rate limiting and no authentication anywhere in the application, so the same caller can also grow
the table without bound: 50 anonymous
POSTrequests increased the row count from 20,008 to 20,058 with noquota, credential or throttle.
One part of the request-side picture is bounded and is recorded accurately: a 300 KB request body was rejected
with
HTTP 413by Express's default 100 kB limit. The gap is therefore in result size, write volume andrequest rate, not in single-request body size.
Affected Component
bezkoder/nodejs-express-mysqlapp/models/tutorial.model.jsgetAll()(noLIMIT/pagination in the query),app/routes/tutorial.routes.js(no throttling middleware),server.js(no rate-limit middleware)GET /api/tutorials,GET /api/tutorials?title=,POST /api/tutorials,DELETE /api/tutorials68d3959Technical Description
There is no
limit/offsetquery parameter, no maximum result size, no per-account quota, and no rate-limitmiddleware in
server.jsor the router. Because no authentication exists, all four endpoints above arereachable anonymously, so both the consumption side (large reads) and the growth side (writes) are open to any
caller.
Data flow:
Attack Preconditions
must first be filled — achievable anonymously, as shown below, or simply by normal application use).
harmless generated text to measure response size.
Step-by-Step Reproduction / PoC
PoC 1 — Unbounded read (least destructive version: one request, no writes)
PoC 2 — Unauthenticated, unthrottled writes (small volume, bounded on purpose)
PoC 3 — Control: request body size is bounded (not a finding)
Observed Result
PoC 1 (
/tmp/opencode/audit-lab-FINAL.log, section 10):PoC 2:
PoC 3 (control):
No service interruption, timeout or crash was produced by these tests; the demonstrated condition is
resource exhaustion risk / unbounded consumption, not a confirmed denial of service.
Expected Secure Result
429 Too Many Requests).LIMIT, statement timeout) and expensive endpoints aremonitored.
Security Impact
Direct impact
An unauthenticated caller can force the application to serialize and transmit arbitrarily large responses (1.5 MB
per request observed at 20k rows, scaling linearly with table size) and can grow storage without limit via
anonymous inserts, increasing disk, CPU, memory and bandwidth consumption per request. Repeated requests amplify
that cost on both the application and the network path.
Amplified impact
anonymous, and the SQL-injection finding's table reads are unaffected by pagination. Fixing this finding
bounds resource use but does not remove anonymous access; fixing authorization does not bound result size.
Evidence
app/models/tutorial.model.jsgetAll()(noLIMIT),app/routes/tutorial.routes.js,server.js(no rate-limit middleware) at commit68d3959./tmp/opencode/audit-lab-FINAL.log, section 10 (1,538,419 bytes /33 ms; 1,537,840 bytes / 29 ms).
HTTP 413)./tmp/opencode/audit-lab.sh.Root Cause
The query layer returns complete result sets with no upper bound, and no middleware enforces request rate,
request count or write quota, so per-request resource cost and total resource growth are both unconstrained and
reachable without credentials.
Remediation
Pair with authentication and a per-account creation quota so storage growth is capped per identity, and return
count/totalmetadata so clients can page without fetching everything.Defense in Depth
GET /api/tutorials/published-style reads.DELETE /api/tutorialsbehind an administrative role so bulk operations cannot be triggered anonymously.HTTP 413) — it is doing its job.Affected Versions / Git History
getAll()has existed without aLIMITsinceapp/models/tutorial.model.jswas created in commitbe702f0(2021-11-10); no rate-limiting or pagination middleware has ever been added toserver.js, whichwas last changed by that same commit.
master, commit68d3959(2024-02-04). No release tags exist, so the affected version isdescribed by commit.