Skip to content

Fix the reported security issue on v5 #2327

Description

@ae2079

we had this report from a user:
Hello Giveth security team,

I believe I found a service vulnerability in the Giveth frontend/backend project update flow. I did not execute this against production data. The evidence below comes from source review plus read-only public GraphQL queries.

Reporter: davethediver
Email: dave.the.diver1010@gmail.com
Ethereum address for reward distribution: 0xa882D8296CCd4eD5A4Ffe4cFaDd1bDf8DFA541D6

Summary

A project owner can store arbitrary HTML in a project update through addProjectUpdate / editProjectUpdate. If that update is selected as a featured project update, the homepage featured update card renders content with dangerouslySetInnerHTML without sanitization.

This creates a stored XSS path for visitors viewing the homepage/featured project update card. A particularly risky path is:

  1. A project owner creates a benign project update.
  2. Giveth admin features that project update via AdminJS.
  3. The project owner later edits the same featured update to include malicious HTML/event attributes.
  4. The homepage fetches the featured update and injects content directly into the DOM.

Impact

JavaScript execution in the giveth.io origin could read same-origin localStorage values, including the app's auth token storage keys, and perform authenticated GraphQL/API actions as the victim. It could also alter donation/project UI, show wallet-phishing prompts, or redirect users from the trusted Giveth origin.

Evidence

Frontend repo: Giveth/giveth-dapps-v2, local HEAD 4cb06d1

The homepage featured update card directly injects raw update HTML:

src/components/views/homepage/projectUpdatesBlock/ProjectUpdateSlide.tsx

<UpdateDesc
  dangerouslySetInnerHTML={{
    __html: update?.content || '',
  }}
/>

That content comes from the public featuredProjectUpdate(projectId) query:

src/apollo/gql/gqlProjects.ts

query featuredProjectUpdate($projectId: Int!) {
  featuredProjectUpdate(projectId: $projectId) {
    id
    title
    projectId
    userId
    content
    isMain
    createdAt
  }
}

Backend repo: Giveth/impact-graph, local HEAD 870dcbc

addProjectUpdate stores the caller-supplied content directly:

src/resolvers/projectResolver.ts

const update = ProjectUpdate.create({
  userId: user.userId,
  projectId: project.id,
  content,
  title,
  createdAt: new Date(),
  isMain: false,
});

const save = await ProjectUpdate.save(update);

editProjectUpdate lets the project owner replace content directly:

update.title = title;
update.content = content;
await update.save();

The entity stores content as a plain string column. The insert/update hook only generates a text summary; it does not sanitize the stored HTML:

src/entities/project.ts

@Field(_type => String)
@Column()
content: string;

@BeforeInsert()
setProjectUpdateContentSummary() {
  this.contentSummary = getHtmlTextSummary(this.content);
}

AdminJS selects existing update rows as featured records by projectUpdateId:

src/server/adminJs/tabs/projectsTab.ts

const featuredProject = FeaturedUpdate.create({
  projectUpdateId: update.id,
  projectId: update.project!.id,
});

await featuredProject.save();

The backend query returns the referenced update row:

src/resolvers/projectResolver.ts

const featuredProject = await FeaturedUpdate.createQueryBuilder(
  'featuredProject',
)
  .innerJoinAndSelect('featuredProject.projectUpdate', 'projectUpdate')
  .where('featuredProject.projectId = :projectId', { projectId })
  .getOne();

return featuredProject!.projectUpdate;

The backend tests also show that arbitrary HTML is preserved in editProjectUpdate responses rather than normalized/sanitized:

src/resolvers/projectResolver.test.ts

content: '<div>TestProjectUpdateAfterUpdateFateme</div>',
...
assert.equal(
  result.data.data.editProjectUpdate.content,
  '<div>TestProjectUpdateAfterUpdateFateme</div>',
);

I also confirmed with a read-only public GraphQL query to https://mainnet.serve.giveth.io/graphql that featuredProjectUpdate.content is returned as raw HTML for currently featured projects.

Safe reproduction outline

Please run this only in a local/staging environment:

  1. Create or seed a project owned by a normal user.
  2. Add a normal update as that project owner.
  3. Feature that update via AdminJS so a FeaturedUpdate row references it.
  4. As the project owner, call:
mutation EditProjectUpdate($updateId: Float!, $title: String!, $content: String!) {
  editProjectUpdate(updateId: $updateId, title: $title, content: $content) {
    id
    title
    content
  }
}

With variables similar to:

{
  "updateId": 123,
  "title": "Update",
  "content": "<p>hello</p><img src=x onerror=\"document.body.setAttribute('data-giveth-xss','1')\">"
}
  1. Visit the homepage/featured projects section where ProjectUpdateSlide renders that featured update.
  2. Observe that the DOM mutation from the event handler occurs. A real attacker could replace that harmless marker with token theft, UI manipulation, or same-origin API calls.

Suggested fixes

Sanitize project update HTML on the backend with a strict allowlist before storing or returning it. The allowlist should permit only the Quill tags/attributes Giveth intentionally supports, and should block event handler attributes, javascript: URLs, dangerous SVG/MathML, and unsafe inline style.

Also consider replacing the dangerouslySetInnerHTML use in ProjectUpdateSlide with the same safe rendering path used elsewhere, or render a text summary on the homepage card instead of raw rich HTML.

Because already-featured updates can be edited after admin selection, please also consider revalidating/sanitizing existing project_update.content rows and either snapshotting reviewed featured content or requiring re-review after a featured update is edited.

Thank you

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions