Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions front/plugins/_publisher_ntfy/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,78 @@
"string": "Enable TLS support. Disable if you are using a self-signed certificate."
}
]
},
{
"function": "URL_QUERY_STRING",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add sample values into the README.md in case people are confused how a valid value should look like

"type": {
"dataType": "string",
"elements": [
{ "elementType": "input", "elementOptions": [{ "type": "password" }], "transformers": [] }
]
},
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "URL query string"
}
],
"description": [
{
"language_code": "en_us",
"string": "Optional query string appended to the ntfy request URL (e.g. <code>p_token=tokenId.tokenValue</code>). Useful when ntfy is behind a reverse proxy or tunnel (Pangolin, Tailscale, ...) that authenticates via a query parameter. Note: values in the URL may be recorded in proxy/server access logs, so for secrets prefer the custom header below. Leave empty to disable."
}
]
},
{
"function": "CUSTOMHEADER_NAME",
"type": {
"dataType": "string",
"elements": [
{ "elementType": "input", "elementOptions": [], "transformers": [] }
]
},
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Custom header name"
}
],
"description": [
{
"language_code": "en_us",
"string": "Optional custom HTTP header name sent with the ntfy request, e.g. to authenticate through a reverse proxy or tunnel. Requires the custom header value to also be set. Leave empty to disable."
}
]
},
{
"function": "CUSTOMHEADER_VALUE",
"type": {
"dataType": "string",
"elements": [
{ "elementType": "input", "elementOptions": [{ "type": "password" }], "transformers": [] }
]
},
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Custom header value"
}
],
"description": [
{
"language_code": "en_us",
"string": "Value for the custom HTTP header defined above. Requires the custom header name to also be set. Leave empty to disable."
}
]
}
]
}
27 changes: 25 additions & 2 deletions front/plugins/_publisher_ntfy/ntfy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import os
import re
import sys
import requests
from base64 import b64encode
Expand Down Expand Up @@ -94,6 +95,11 @@ def send(html, text):
user = get_setting_value('NTFY_USER')
pwd = get_setting_value('NTFY_PASSWORD')
verify_ssl = get_setting_value('NTFY_VERIFY_SSL')
custom_header_name = get_setting_value('NTFY_CUSTOMHEADER_NAME')
custom_header_value = get_setting_value('NTFY_CUSTOMHEADER_VALUE')
# Strip a leading '?' so both "p_token=..." and "?p_token=..." work; requests
# adds the '?' itself, and a leading one would produce a broken "??" in the URL.
url_query_string = get_setting_value('NTFY_URL_QUERY_STRING').lstrip('?')

# prepare request headers
headers = {
Expand All @@ -112,13 +118,23 @@ def send(html, text):
# add authorization header with hash
headers["Authorization"] = "Basic {}".format(basichash)

# Optional custom header, e.g. to authenticate through a reverse proxy / tunnel
# (Pangolin, Tailscale, ...) sitting in front of the ntfy instance. Skip it if it
# would clobber a built-in header (e.g. Authorization) so ntfy auth stays intact.
if custom_header_name != '' and custom_header_value != '':
if custom_header_name.lower() in {k.lower() for k in headers}:
mylog('none', [f'[{pluginName}] ⚠ Custom header "{custom_header_name}" collides with a built-in header; skipping it.'])
else:
headers[custom_header_name] = custom_header_value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how the repository's installed requests version renders invalid header values.
python - <<'PY'
import inspect
import requests
from requests.exceptions import InvalidHeader, RequestException

print("InvalidHeader is RequestException:", issubclass(InvalidHeader, RequestException))
print(inspect.getsource(requests.utils._validate_header_part))
PY

Repository: netalertx/NetAlertX

Length of output: 1154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant function and nearby error handling in front/plugins/_publisher_ntfy/ntfy.py.
python3 - <<'PY'
from pathlib import Path

path = Path("front/plugins/_publisher_ntfy/ntfy.py")
lines = path.read_text().splitlines()
for start, end in [(1, 240)]:
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: netalertx/NetAlertX

Length of output: 7586


Redact the custom header value from request errors. InvalidHeader includes the offending header value in the exception text, so a bad NTFY_CUSTOMHEADER_VALUE can leak into mylog(...) and the persisted response_text. Redact it before logging or returning the exception.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/plugins/_publisher_ntfy/ntfy.py` at line 128, The custom header value
can leak through InvalidHeader exceptions when building the request headers in
ntfy.py. Update the header-setting and error-handling path around the
headers[custom_header_name] assignment (and the code that logs via mylog(...) or
persists response_text) so any exception text is sanitized before being logged
or returned, specifically redacting the offending NTFY_CUSTOMHEADER_VALUE while
preserving the rest of the error context.


# call NTFY service
try:
response = requests.post("{}/{}".format(
get_setting_value('NTFY_HOST'),
get_setting_value('NTFY_TOPIC')),
data = text,
headers = headers,
params = url_query_string if url_query_string != '' else None,
verify = verify_ssl,
timeout = get_setting_value('NTFY_RUN_TIMEOUT')
)
Expand All @@ -132,9 +148,16 @@ def send(html, text):
response_text = json.dumps(response.text)

except requests.exceptions.RequestException as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR: ', e])
# The exception message embeds the request URL, which may include a secret
# query string (e.g. a proxy token). Redact the query part before it is
# logged and persisted to the plugin result file / shown in the UI.
error_text = str(e)
if url_query_string != '':
error_text = re.sub(r'(\?)\S+', r'\1<redacted>', error_text)

mylog('none', [f'[{pluginName}] ⚠ ERROR: ', error_text])

response_text = e
response_text = error_text

return response_text, response_status_code

Expand Down
Loading