VYPR
High severityNVD Advisory· Published Oct 24, 2024· Updated Oct 28, 2024

Reflected cross-site scripting vulnerability (XSS) in GData extension (authorized.vt)

CVE-2024-47878

Description

OpenRefine is a free, open source tool for working with messy data. Prior to version 3.8.3, the /extension/gdata/authorized endpoint includes the state GET parameter verbatim in a `` tag in the output, so without escaping. An attacker could lead or redirect a user to a crafted URL containing JavaScript code, which would then cause that code to be executed in the victim's browser as if it was part of OpenRefine. Version 3.8.3 fixes this issue.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

OpenRefine before 3.8.3 has a reflected XSS in the GData extension's /authorized endpoint due to unescaped state parameter.

Vulnerability

CVE-2024-47878 is a reflected cross-site scripting (XSS) vulnerability in OpenRefine, a popular tool for cleaning messy data. Prior to version 3.8.3, the /extension/gdata/authorized endpoint in the GData extension directly reflected the state GET parameter into a `` tag without proper HTML escaping or sanitization. This allowed an attacker to inject arbitrary JavaScript code into the response.[1][4]

Exploitation

An attacker could craft a malicious URL containing JavaScript payload in the state parameter and trick a victim into clicking it (e.g., via phishing or redirection). The exploit requires no special configuration; the GData extension must be present, but the attacker does not need valid OAuth credentials. The proof-of-concept URL http://localhost:3333/extension/gdata/authorized?state=%22,alert(1),%22&error= demonstrates that the code executes immediately in the victim's browser context.[4] The fix introduced validation of the state parameter format, ensuring it is a valid base64-encoded JSON with expected winname and cb patterns, rejecting malformed input.[2]

Impact

Successful exploitation allows arbitrary JavaScript execution in the user's browser. The attacker can perform any action the user can, such as deleting projects, accessing database passwords, or executing Jython or Closure expressions if those extensions are enabled. This can lead to complete compromise of the OpenRefine instance and its data.[1][4]

Mitigation

OpenRefine version 3.8.3, released in October 2024, contains the fix. Users should upgrade immediately. No workaround is available. The vulnerability is not listed in CISA's Known Exploited Vulnerabilities catalog at the time of publication.[1][2][3]

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.openrefine:extensionsMaven
< 3.8.33.8.3

Affected products

3

Patches

1
10bf0874d67f

gdata: check cb parameter in authorized command

https://github.com/OpenRefine/OpenRefineAntonin DelpeuchAug 17, 2024via ghsa
3 files changed · +94 21
  • extensions/gdata/module/authorized.vt+2 4 modified
    @@ -40,10 +40,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       <span id="gdata-authorized"></span>
       
       <script>
    -    var state = JSON.parse(window.atob("$state"));
    -    
    -    var windowName = state.winname;
    -    var callbackName = state.cb;
    +    var windowName = "$winname";
    +    var callbackName = "$cb";
         
         var w = window.open("", windowName);
         var callback = w[callbackName];
    
  • extensions/gdata/module/MOD-INF/controller.js+4 17 modified
    @@ -101,23 +101,10 @@ function process(path, request, response) {
         
         send(request, response, "authorize.vt", context);
       } else if (path == "authorized") {
    -    var context = {};
    -    context.state = request.getParameter("state");
    -    
    -    (function() {
    -      if (Packages.com.google.refine.extension.gdata.TokenCookie.getToken(request) !== null) {
    -          return;
    -      }
    -      var tokenAndExpiresInSeconds =  Packages.com.google.refine.extension.gdata.GoogleAPIExtension.getTokenFromCode(module,request);
    -      if (tokenAndExpiresInSeconds) {
    -        var tokenInfo = tokenAndExpiresInSeconds.split(",");
    -        Packages.com.google.refine.extension.gdata.TokenCookie.setToken(request, response, tokenInfo[0], tokenInfo[1]);
    -        return;
    -      }
    -      Packages.com.google.refine.extension.gdata.TokenCookie.deleteToken(request, response);
    -    })();
    -    
    -    send(request, response, "authorized.vt", context);
    +    // it's a command but we handle it manually here, so as to preserve the URL
    +    var command = new Packages.com.google.refine.extension.gdata.AuthorizedCommand(module);
    +    command.doGet(request, response);
    +    butterfly.responded();
       } else if (path == "/" || path == "") {
           var context = {};
           context.version = version;
    
  • extensions/gdata/src/com/google/refine/extension/gdata/AuthorizedCommand.java+88 0 added
    @@ -0,0 +1,88 @@
    +
    +package com.google.refine.extension.gdata;
    +
    +import java.io.IOException;
    +import java.util.Base64;
    +import java.util.regex.Pattern;
    +
    +import javax.servlet.ServletException;
    +import javax.servlet.http.HttpServletRequest;
    +import javax.servlet.http.HttpServletResponse;
    +
    +import com.fasterxml.jackson.databind.JsonNode;
    +import com.fasterxml.jackson.databind.node.ObjectNode;
    +import edu.mit.simile.butterfly.ButterflyModule;
    +import org.apache.velocity.VelocityContext;
    +
    +import com.google.refine.commands.Command;
    +import com.google.refine.util.ParsingUtilities;
    +
    +public class AuthorizedCommand extends Command {
    +
    +    Pattern callbackPattern = Pattern.compile("^cb[0-9]+$");
    +    Pattern winnamePattern = Pattern.compile("^openrefine[0-9]+$");
    +
    +    ButterflyModule module;
    +
    +    public AuthorizedCommand(ButterflyModule module) {
    +        this.module = module;
    +    }
    +
    +    @Override
    +    public void doGet(HttpServletRequest request, HttpServletResponse response)
    +            throws ServletException, IOException {
    +        VelocityContext velocityContext = new VelocityContext();
    +        String state = request.getParameter("state");
    +        if (state == null) {
    +            respond(response, "error", "No 'state' parameter provided");
    +            return;
    +        }
    +        try {
    +            byte[] decoded = Base64.getDecoder().decode(state);
    +            JsonNode parsed = ParsingUtilities.mapper.readTree(decoded);
    +            if (parsed instanceof ObjectNode && parsed.has("winname") && parsed.has("cb")) {
    +                ObjectNode object = (ObjectNode) parsed;
    +                String cb = object.get("cb").asText();
    +                String winname = object.get("winname").asText();
    +                if (!winnamePattern.matcher(winname).find()) {
    +                    respond(response, "error", "Invalid winname provided");
    +                    return;
    +                }
    +                if (!callbackPattern.matcher(cb).find()) {
    +                    respond(response, "error", "Invalid callback provided");
    +                    return;
    +                }
    +                velocityContext.internalPut("winname", winname);
    +                velocityContext.internalPut("cb", cb);
    +
    +            } else {
    +                throw new IllegalArgumentException("expected a JSON object");
    +            }
    +        } catch (IllegalArgumentException | IOException e) {
    +            respond(response, "error", "Invalid 'state' parameter provided");
    +            return;
    +        }
    +
    +        updateToken(request, response);
    +
    +        try {
    +            module.sendTextFromTemplate(request, response, velocityContext, "authorized.vt", "UTF-8", "text/html", false);
    +        } catch (Exception e) {
    +            respondException(response, e);
    +        }
    +    }
    +
    +    private void updateToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
    +        if (TokenCookie.getToken(request) != null) {
    +            return;
    +        }
    +        var tokenAndExpiresInSeconds = GoogleAPIExtension.getTokenFromCode(module, request);
    +        if (tokenAndExpiresInSeconds != null) {
    +            var tokenInfo = tokenAndExpiresInSeconds.split(",");
    +            TokenCookie.setToken(request, response, tokenInfo[0], tokenInfo[1]);
    +            return;
    +        }
    +        TokenCookie.deleteToken(request, response);
    +    }
    +
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.