diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 67507d1f..76277109 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -248,7 +248,8 @@ async def m007_set_invoice_expiries(db: Connection): """, {"expiry": expiration_date, "checking_id": checking_id}, ) - except Exception: + except Exception as exc: + logger.debug(exc) continue except OperationalError: # this is necessary now because it may be the case that this migration will @@ -371,7 +372,8 @@ async def m014_set_deleted_wallets(db: Connection): "wallet": row.get("id"), }, ) - except Exception: + except Exception as exc: + logger.debug(exc) continue except OperationalError: # this is necessary now because it may be the case that this migration will diff --git a/lnbits/core/services/lnurl.py b/lnbits/core/services/lnurl.py index 4b09a17a..6592d4a0 100644 --- a/lnbits/core/services/lnurl.py +++ b/lnbits/core/services/lnurl.py @@ -67,16 +67,16 @@ async def redeem_lnurl_withdraw( external=True, wal=wallet_id, ) - except Exception: - pass + except Exception as exc: + logger.debug(exc) headers = {"User-Agent": settings.user_agent} async with httpx.AsyncClient(headers=headers) as client: try: check_callback_url(res["callback"]) await client.get(res["callback"], params=params) - except Exception: - pass + except Exception as exc: + logger.debug(exc) async def perform_lnurlauth( diff --git a/lnbits/core/views/auth_api.py b/lnbits/core/views/auth_api.py index 62edebd7..92805323 100644 --- a/lnbits/core/views/auth_api.py +++ b/lnbits/core/views/auth_api.py @@ -557,8 +557,8 @@ def _find_auth_provider_class(provider: str) -> Callable: provider_class = getattr(provider_module, f"{provider.title()}SSO") if provider_class: return provider_class - except Exception: - pass + except Exception as exc: + logger.debug(exc) raise ValueError(f"No SSO provider found for '{provider}'.") diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 258d5bdb..00bdde38 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -306,8 +306,11 @@ def check_callback_url(url: str): ) -def download_url(url, save_path): - with request.urlopen(url, timeout=60) as dl_file: +def download_url(url: str, save_path: Path): + if not url.startswith(("http:", "https:")): + raise ValueError(f"Invalid URL: {url}. Must start with 'http' or 'https'.") + + with request.urlopen(url, timeout=60) as dl_file: # noqa: S310 with open(save_path, "wb") as out_file: out_file.write(dl_file.read()) diff --git a/lnbits/settings.py b/lnbits/settings.py index 188f4801..6828b48a 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -683,7 +683,7 @@ class NodeUISettings(LNbitsSettings): class AuthMethods(Enum): user_id_only = "user-id-only" - username_and_password = "username-password" + username_and_password = "username-password" # noqa: S105 nostr_auth_nip98 = "nostr-auth-nip98" google_auth = "google-auth" github_auth = "github-auth" diff --git a/lnbits/wallets/breez.py b/lnbits/wallets/breez.py index 87f80a62..2dc54e50 100644 --- a/lnbits/wallets/breez.py +++ b/lnbits/wallets/breez.py @@ -57,8 +57,8 @@ else: # else convert from base64 try: return base64.b64decode(source) - except Exception: - pass + except Exception as exc: + logger.debug(exc) return None def load_greenlight_credentials() -> ( diff --git a/lnbits/wallets/cliche.py b/lnbits/wallets/cliche.py index a1055cee..0a97b542 100644 --- a/lnbits/wallets/cliche.py +++ b/lnbits/wallets/cliche.py @@ -182,7 +182,8 @@ class ClicheWallet(Wallet): try: if data["result"]["status"]: yield data["result"]["payment_hash"] - except Exception: + except Exception as exc: + logger.debug(exc) continue except Exception as exc: logger.error( diff --git a/lnbits/wallets/corelightning.py b/lnbits/wallets/corelightning.py index 5fbfb3ee..d414d42a 100644 --- a/lnbits/wallets/corelightning.py +++ b/lnbits/wallets/corelightning.py @@ -1,6 +1,6 @@ import asyncio -import random from collections.abc import AsyncGenerator +from secrets import token_urlsafe from typing import Any, Optional from bolt11.decode import decode as bolt11_decode @@ -92,7 +92,7 @@ class CoreLightningWallet(Wallet): unhashed_description: Optional[bytes] = None, **kwargs, ) -> InvoiceResponse: - label = kwargs.get("label", f"lbl{random.random()}") + label = kwargs.get("label", f"lbl{token_urlsafe(16)}") msat: int = int(amount * 1000) try: if description_hash and not unhashed_description: diff --git a/lnbits/wallets/corelightningrest.py b/lnbits/wallets/corelightningrest.py index 842927d7..fe6ded01 100644 --- a/lnbits/wallets/corelightningrest.py +++ b/lnbits/wallets/corelightningrest.py @@ -1,7 +1,7 @@ import asyncio import json -import random from collections.abc import AsyncGenerator +from secrets import token_urlsafe from typing import Optional import httpx @@ -111,7 +111,7 @@ class CoreLightningRestWallet(Wallet): unhashed_description: Optional[bytes] = None, **kwargs, ) -> InvoiceResponse: - label = kwargs.get("label", f"lbl{random.random()}") + label = kwargs.get("label", f"lbl{token_urlsafe(16)}") data: dict = { "amount": amount * 1000, "description": memo, @@ -298,7 +298,8 @@ class CoreLightningRestWallet(Wallet): self.last_pay_index = inv["pay_index"] if not paid: continue - except Exception: + except Exception as exc: + logger.debug(exc) continue logger.trace(f"paid invoice: {inv}") diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index 447aefe2..6231fe5a 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -292,7 +292,8 @@ class LndRestWallet(Wallet): ) else: return PaymentPendingStatus() - except Exception: + except Exception as exc: + logger.debug(exc) continue return PaymentPendingStatus() @@ -307,7 +308,8 @@ class LndRestWallet(Wallet): inv = json.loads(line)["result"] if not inv["settled"]: continue - except Exception: + except Exception as exc: + logger.debug(exc) continue payment_hash = base64.b64decode(inv["r_hash"]).hex() diff --git a/lnbits/wallets/lntips.py b/lnbits/wallets/lntips.py index b434f842..8edf8e50 100644 --- a/lnbits/wallets/lntips.py +++ b/lnbits/wallets/lntips.py @@ -173,11 +173,12 @@ class LnTipsWallet(Wallet): inv = json.loads(data) if not inv.get("payment_hash"): continue - except Exception: + except Exception as exc: + logger.debug(exc) continue yield inv["payment_hash"] - except Exception: - pass + except Exception as exc: + logger.debug(exc) # do not sleep if the connection was active for more than 10s # since the backend is expected to drop the connection after 90s diff --git a/lnbits/wallets/macaroon/macaroon.py b/lnbits/wallets/macaroon/macaroon.py index 674a4012..e445677a 100644 --- a/lnbits/wallets/macaroon/macaroon.py +++ b/lnbits/wallets/macaroon/macaroon.py @@ -2,6 +2,8 @@ import base64 from getpass import getpass from typing import Optional +from loguru import logger + from lnbits.utils.crypto import AESCipher @@ -39,7 +41,7 @@ def load_macaroon( try: macaroon = base64.b64decode(macaroon).hex() return macaroon - except Exception: - pass + except Exception as exc: + logger.debug(exc) return macaroon diff --git a/lnbits/wallets/nwc.py b/lnbits/wallets/nwc.py index e6b3b9f8..b15f6a13 100644 --- a/lnbits/wallets/nwc.py +++ b/lnbits/wallets/nwc.py @@ -390,7 +390,7 @@ class NWCConnection: n = max_length - len(subid) if n > 0: for _ in range(n): - subid += chars[random.randint(0, len(chars) - 1)] + subid += chars[random.randint(0, len(chars) - 1)] # noqa: S311 return subid async def _close_subscription_by_subid( diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py index 35ebb1bd..2fa6b56d 100644 --- a/lnbits/wallets/spark.py +++ b/lnbits/wallets/spark.py @@ -1,8 +1,8 @@ import asyncio import hashlib import json -import random from collections.abc import AsyncGenerator +from secrets import token_urlsafe from typing import Optional import httpx @@ -116,7 +116,7 @@ class SparkWallet(Wallet): unhashed_description: Optional[bytes] = None, **kwargs, ) -> InvoiceResponse: - label = f"lbs{random.random()}" + label = f"lbs{token_urlsafe(16)}" try: if description_hash: r = await self.invoicewithdescriptionhash( diff --git a/poetry.lock b/poetry.lock index 4c074ca5..7dee5335 100644 --- a/poetry.lock +++ b/poetry.lock @@ -363,6 +363,31 @@ files = [ {file = "backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d"}, ] +[[package]] +name = "bandit" +version = "1.8.5" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "bandit-1.8.5-py3-none-any.whl", hash = "sha256:cb2e57524e99e33ced48833c6cc9c12ac78ae970bb6a450a83c4b506ecc1e2f9"}, + {file = "bandit-1.8.5.tar.gz", hash = "sha256:db812e9c39b8868c0fed5278b77fffbbaba828b4891bc80e34b9c50373201cfd"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] +toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] +yaml = ["PyYAML"] + [[package]] name = "base58" version = "2.1.1" @@ -2094,7 +2119,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -2211,7 +2236,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -2581,6 +2606,21 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pbr" +version = "6.1.1" +description = "Python Build Reasonableness" +optional = false +python-versions = ">=2.6" +groups = ["dev"] +files = [ + {file = "pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76"}, + {file = "pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "platformdirs" version = "4.3.8" @@ -3344,7 +3384,7 @@ version = "14.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, @@ -3753,6 +3793,21 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] +[[package]] +name = "stevedore" +version = "5.4.1" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe"}, + {file = "stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b"}, +] + +[package.dependencies] +pbr = ">=2.0.0" + [[package]] name = "tlv8" version = "0.10.0" @@ -4420,4 +4475,4 @@ liquid = ["wallycore"] [metadata] lock-version = "2.1" python-versions = "~3.12 | ~3.11 | ~3.10" -content-hash = "53f582a8079540033939ccd1bbd93b8ec1e8190ee26be0c0b8d64d57edb5cdac" +content-hash = "17a61e0f0d45c02d99c398f8ddfd68f7bd55a04183cad7d615e170813be43348" diff --git a/pyproject.toml b/pyproject.toml index 4d018b14..9fb55941 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ breez = ["breez-sdk", "breez-sdk-liquid"] liquid = ["wallycore"] [tool.poetry.group.dev.dependencies] +bandit = "^1.8.5" black = "^25.1.0" mypy = "^1.11.2" types-protobuf = "^6.30.2.20250516" @@ -192,7 +193,8 @@ extend-exclude = [ # UP - pyupgrade # RUF - ruff specific rules # B - bugbear -select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"] +# S - bandit +select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B", "S"] # UP007: pyupgrade: use X | Y instead of Optional. (python3.10) # RUF012: mutable-class-default ignore = ["RUF012"] @@ -211,6 +213,28 @@ classmethod-decorators = [ "validator", ] +[tool.ruff.lint.per-file-ignores] +# S101: Use of assert detected. mostly for tests... +# S105: Use of hard-coded password. mostly for tests... +# S106: Possible hardcoded password: 'password'. +# S307 Use of possibly insecure function; consider using `ast.literal_eval +# S602 `subprocess` call with `shell=True` identified, security issue +# S603 `subprocess` call: check for execution of untrusted input +# S607: Starting a process with a partial executable path +# TODO: do not skip S608: +# S608: Possible SQL injection vector through string-based query construction +# S324 Probable use of insecure hash functions in `hashlib`: `md5` +"lnbits/*" = ["S101", "S608"] +"lnbits/core/views/admin_api.py" = ["S602", "S603", "S607"] +"crypto.py" = ["S324"] +"test*.py" = ["S101", "S105", "S106", "S307"] +"tools*.py" = ["S101", "S608"] +"tests/*" = ["S311"] +"tests/regtest/helpers.py" = ["S603"] + +[tool.bandit] +skips = ["B101", "B404"] + [tool.ruff.lint.mccabe] max-complexity = 10