Skip to content

Commit f19b975

Browse files
committed
fix: security enhancement
less permissive file permissions, t/o for requests etc etc
1 parent fb026d6 commit f19b975

8 files changed

Lines changed: 363 additions & 28 deletions

File tree

Dockerfile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@ ENV TZ=Europe/Paris
77
WORKDIR /app
88

99
# Crée l'utilisateur avant de faire le chown
10-
RUN adduser --disabled-password --gecos "" appuser
10+
RUN adduser --disabled-password --gecos "" --shell /sbin/nologin appuser
1111

1212
# Création du dossier /config avec les bonnes permissions pour appuser
1313
RUN mkdir -p /config && chown -R appuser /app /config
1414

1515
COPY requirements.txt .
16-
RUN pip install --no-cache-dir -r requirements.txt
16+
RUN pip install --no-cache-dir --require-hashes -r requirements.txt
1717

1818
COPY . .
1919

2020
COPY entrypoint.sh /entrypoint.sh
21-
RUN chmod +x /entrypoint.sh
21+
RUN chmod +x /entrypoint.sh && chmod 0755 /entrypoint.sh
2222

2323
RUN apk add --no-cache su-exec
2424

25+
USER appuser
26+
2527
ENTRYPOINT ["/entrypoint.sh"]
2628
CMD ["python", "src/main.py"]
2729
LABEL org.opencontainers.image.source="https://github.com/leonpwd/NotifyNotes"

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ NTFY_URL=https://ntfy.sh/mon-topic
7777
LOG_LEVEL=DEBUG
7878
```
7979

80+
### Installation locale
81+
82+
```bash
83+
# Créer un environnement virtuel
84+
python3 -m venv venv
85+
source venv/bin/activate
86+
87+
# Installer les dépendances avec vérification des hashes
88+
pip install --no-cache-dir --require-hashes -r requirements.txt
89+
90+
# Lancer le script
91+
python src/main.py
92+
```
93+
8094
## 🤝 Contribuer
8195

8296
Contributions bienvenues ! Ouvrez une issue ou une pull request.

entrypoint.sh

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
#!/bin/sh
22
# filepath: entrypoint.sh
33

4-
# Changer le propriétaire du dossier monté
5-
chown -R 1000:1000 /config
4+
set -e
65

7-
# Exécuter l’application avec l’utilisateur non-root
8-
exec su-exec 1000:1000 "$@"
6+
# Récupérer l'UID/GID de appuser dynamiquement
7+
APPUSER_UID=$(id -u appuser 2>/dev/null || echo 1000)
8+
APPUSER_GID=$(id -g appuser 2>/dev/null || echo 1000)
9+
10+
# Changer le propriétaire du dossier monté avec les bonnes permissions
11+
chown -R ${APPUSER_UID}:${APPUSER_GID} /config
12+
13+
# Exécuter l'application avec l'utilisateur non-root
14+
exec su-exec ${APPUSER_UID}:${APPUSER_GID} "$@"

requirements.txt

Lines changed: 291 additions & 3 deletions
Large diffs are not rendered by default.

src/compare_json.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ def save_notes_json(data, filepath):
1313
data.replace('�', 'é').replace('é', 'é').replace('è', 'è').replace('�', 'Á')
1414
with open(filepath, "w", encoding="utf-8") as f:
1515
json.dump(data, f, ensure_ascii=False, indent=2)
16+
# Restreindre les permissions du fichier JSON (lecture/écriture propriétaire uniquement)
17+
os.chmod(filepath, 0o600)
1618

1719
def find_new_notes(old_notes, new_notes):
1820
changes = []

src/env.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import os
22
import random
3+
import re
34

45
ERROR_HASH = "9c287ec0f172e07215c5af2f96445968c266bcc24519ee0cf70f43f178fa613e"
56

7+
def validate_ntfy_url(url):
8+
"""Valider le format d'une URL ntfy"""
9+
pattern = r'^https?://[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}/[a-zA-Z0-9_\-]+$'
10+
return re.match(pattern, url) is not None
11+
612
#* Activer ou non le dotenv si le fichier .env est présent
713
if os.path.exists(".env"):
814
try:
@@ -46,13 +52,22 @@
4652
#? Endpoint de notification NTFY
4753
NTFY_URL = os.getenv("NTFY_URL")
4854
if NTFY_URL:
55+
if not validate_ntfy_url(NTFY_URL):
56+
print("Erreur: L'URL NTFY est invalide. Format attendu: https://domain.com/topic")
57+
exit(1)
4958
print("URL ntfy custom utilisée :", NTFY_URL,"\n")
5059
NTFY_URL_LOCAL_FALLBACK = os.getenv("NTFY_URL_LOCAL_FALLBACK", None)
60+
if NTFY_URL_LOCAL_FALLBACK and not validate_ntfy_url(NTFY_URL_LOCAL_FALLBACK):
61+
print("Erreur: L'URL NTFY_LOCAL_FALLBACK est invalide.")
62+
exit(1)
5163
else:
5264
# Si pas de variable d'env, on regarde dans le fichier STORAGE_FILE_URL
5365
if os.path.exists(STORAGE_FILE_URL):
5466
with open(STORAGE_FILE_URL, "r") as f:
5567
NTFY_URL = f.read().strip()
68+
if not validate_ntfy_url(NTFY_URL):
69+
print("Erreur: L'URL NTFY stockée est invalide. Supprimez le fichier et relancez.")
70+
exit(1)
5671
print("URL ntfy récupérée depuis le fichier :", NTFY_URL,"\n")
5772
NTFY_AUTH = False
5873
auth = None
@@ -62,6 +77,7 @@
6277
NTFY_URL = f"https://ntfy.sh/notes-{random_suffix}"
6378
with open(STORAGE_FILE_URL, "w") as f:
6479
f.write(NTFY_URL)
80+
os.chmod(STORAGE_FILE_URL, 0o600) # Restreindre les permissions
6581
print("URL ntfy par défaut générée :", NTFY_URL,"\n")
6682
NTFY_AUTH = False
6783
auth = None

src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def get_notes_content():
3838
'Sec-Fetch-User': '?1',
3939
'Cache-Control': 'max-age=0'
4040
}
41-
response = requests.get(URL, headers=headers)
41+
response = requests.get(URL, headers=headers, timeout=30)
4242

4343
if response.status_code != 200:
4444
raise Exception(f"Erreur lors de la récupération des notes: {response.status_code} - {response.text}")

src/parse.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,31 @@ def convert_notes_to_json(url_response, json_file):
4040
if not html_content:
4141
raise ValueError("Le contenu HTML est vide ou invalide.")
4242

43-
soup = BeautifulSoup(html_content, "lxml")
44-
45-
46-
thead = soup.find("thead")
47-
if thead is None:
48-
print("Avertissement : balise <thead> non trouvée dans la réponse, le serveur est probablement en train de se reload, attente 1 minutes avant relance...")
49-
with open("debug_last_notes.html", "w", encoding="utf-8") as f:
50-
f.write(html_content)
51-
time.sleep(60)
52-
print("Redémarrage du script...")
53-
sys.exit(1)
54-
header_row = thead.find_all("tr")[1]
55-
headers = [fix_encoding_accents(th.get_text(separator=" ", strip=True).split("\n")[0]) for th in header_row.find_all("th")]
43+
try:
44+
soup = BeautifulSoup(html_content, "lxml")
45+
46+
thead = soup.find("thead")
47+
if thead is None:
48+
print("Avertissement : balise <thead> non trouvée dans la réponse, le serveur est probablement en train de se reload, attente 1 minutes avant relance...")
49+
# Sauvegarder en debug uniquement si LOG_LEVEL == DEBUG
50+
import os
51+
if os.getenv("LOG_LEVEL", "INFO").upper() == "DEBUG":
52+
try:
53+
with open("debug_last_notes.html", "w", encoding="utf-8") as f:
54+
f.write(html_content[:10000]) # Limiter à 10KB pour éviter les gros fichiers
55+
os.chmod("debug_last_notes.html", 0o600) # Restreindre les permissions
56+
except Exception as e:
57+
print(f"Impossible de sauvegarder le fichier de debug: {e}")
58+
time.sleep(60)
59+
print("Redémarrage du script...")
60+
sys.exit(1)
61+
header_row = thead.find_all("tr")[1]
62+
headers = [fix_encoding_accents(th.get_text(separator=" ", strip=True).split("\n")[0]) for th in header_row.find_all("th")]
5663

57-
rows = [
58-
[fix_encoding_accents(td.get_text(separator=" ", strip=True)) for td in row.find_all("td")]
59-
for row in soup.find("tbody").find_all("tr")
60-
if "master-1" in row.get("class", [])
64+
rows = [
65+
[fix_encoding_accents(td.get_text(separator=" ", strip=True)) for td in row.find_all("td")]
66+
for row in soup.find("tbody").find_all("tr")
67+
if "master-1" in row.get("class", [])
6168
]
6269

6370
data = [dict(zip(headers, cells)) for cells in rows if any(cells[1:])]

0 commit comments

Comments
 (0)