Where: src/graphql/schema.ts, updateProjectScores resolver
(lines ~236-255):
updateProjectScores(id: ID!, creditQuality: Int!, greenImpact: Int!): Project!
updateProjectScores: async ({ id, creditQuality, greenImpact }, context) => {
if (!context.isAdmin) { throw new Error("Unauthorized: Admin access required"); }
const projectId = parseInt(id, 10);
const tx_hash = await updateImpactScore(projectId, creditQuality, greenImpact);
recordAudit({ project_id: projectId, credit_quality: creditQuality, green_impact: greenImpact, tx_hash, triggered_by: "graphql" });
return new ProjectResolver(id);
},
What's wrong: every other place credit_quality/green_impact values
get produced in this codebase goes through lib/scoring.ts's
computeScores(), which clamps both to [SCORE_MIN, SCORE_MAX] =
[0, 100] by construction. This GraphQL mutation is the one exception —
it takes creditQuality/greenImpact directly as caller-supplied Int
arguments and passes them straight to updateImpactScore() (which builds
and submits the actual Soroban update_impact_score transaction), with no
range check at all. An admin caller (or anyone who obtains admin
credentials, e.g. via any of the several admin-auth weaknesses filed
separately in this repo) can call:
mutation { updateProjectScores(id: "1", creditQuality: 999999, greenImpact: -1) { id } }
and have those exact out-of-range values written on-chain and into the
audit log (recordAudit records whatever was passed, not a
clamped/validated version), corrupting the [0, 100] invariant that every
other reader of these fields (REST responses, computeAggregateScores,
compareROI, benchmarking thresholds, etc.) assumes holds. Also unlike
the sibling project query resolver (which checks
if (projectId < 1 || projectId > total) return null;), this mutation
never validates projectId is in range before submitting the transaction.
Suggested fix: validate creditQuality/greenImpact are integers in
[0, 100] (return a GraphQL error otherwise) and projectId is in
[1, total], matching the guarantees computeScores() and the project
query resolver already provide elsewhere.
Where:
src/graphql/schema.ts,updateProjectScoresresolver(lines ~236-255):
What's wrong: every other place
credit_quality/green_impactvaluesget produced in this codebase goes through
lib/scoring.ts'scomputeScores(), which clamps both to[SCORE_MIN, SCORE_MAX]=[0, 100]by construction. This GraphQL mutation is the one exception —it takes
creditQuality/greenImpactdirectly as caller-suppliedIntarguments and passes them straight to
updateImpactScore()(which buildsand submits the actual Soroban
update_impact_scoretransaction), with norange check at all. An admin caller (or anyone who obtains admin
credentials, e.g. via any of the several admin-auth weaknesses filed
separately in this repo) can call:
and have those exact out-of-range values written on-chain and into the
audit log (
recordAuditrecords whatever was passed, not aclamped/validated version), corrupting the
[0, 100]invariant that everyother reader of these fields (REST responses,
computeAggregateScores,compareROI, benchmarking thresholds, etc.) assumes holds. Also unlikethe sibling
projectquery resolver (which checksif (projectId < 1 || projectId > total) return null;), this mutationnever validates
projectIdis in range before submitting the transaction.Suggested fix: validate
creditQuality/greenImpactare integers in[0, 100](return a GraphQL error otherwise) andprojectIdis in[1, total], matching the guaranteescomputeScores()and theprojectquery resolver already provide elsewhere.