From 7227e3273fab4f9e807dab9800f1e601c354b58e Mon Sep 17 00:00:00 2001 From: jamilahmadzai Date: Sat, 16 May 2026 11:05:05 +0200 Subject: [PATCH 1/2] Harden SMS provider configuration --- common/components/SMSComponent.php | 58 ++++++++++++++++++++--- common/config/main.php | 6 ++- docs/setup.md | 16 ++++++- scripts/check-sms-provider-hardening.py | 62 +++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 scripts/check-sms-provider-hardening.py diff --git a/common/components/SMSComponent.php b/common/components/SMSComponent.php index a8f2d218..981b721b 100644 --- a/common/components/SMSComponent.php +++ b/common/components/SMSComponent.php @@ -2,6 +2,7 @@ namespace common\components; use yii\base\Component; +use yii\base\InvalidConfigException; use yii\httpclient\Client; /** @@ -10,9 +11,53 @@ class SMSComponent extends Component { /** - * @var string Variable for test api key to be stored in + * @var string SMS provider endpoint URL */ - private $apiEndpoint = "http://62.215.226.164/fccsms.aspx"; + public $apiEndpoint; + + /** + * @var string SMS provider username + */ + public $username; + + /** + * @var string SMS provider password + */ + public $password; + + /** + * @var string SMS sender name + */ + public $sender; + + /** + * @var string SMS provider language flag + */ + public $language = 'L'; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + + foreach (['apiEndpoint', 'username', 'password', 'sender'] as $attribute) { + if (!is_string($this->$attribute) || trim($this->$attribute) === '') { + throw new InvalidConfigException(strtr('"{class}::{attribute}" cannot be empty.', [ + '{class}' => static::class, + '{attribute}' => '$' . $attribute + ])); + } + } + + if (stripos($this->apiEndpoint, 'https://') !== 0) { + throw new InvalidConfigException(strtr('"{class}::{attribute}" must use HTTPS.', [ + '{class}' => static::class, + '{attribute}' => '$apiEndpoint' + ])); + } + } /** * Send SMS @@ -23,12 +68,12 @@ public function sendSms($phone_number, $message) $phone_number = str_replace(' ', '', $phone_number); $smsParams = [ - "UID" => "usrbawes", - "p" => "bawes1452", - "S" => "Plugn", + "UID" => $this->username, + "p" => $this->password, + "S" => $this->sender, "G" => $phone_number, "M" => $message, - "L" => "L", + "L" => $this->language, ]; $client = new Client(); @@ -37,6 +82,7 @@ public function sendSms($phone_number, $message) ->setMethod('POST') ->setUrl($this->apiEndpoint) ->setData($smsParams) + ->setOptions(['timeout' => 10]) ->send(); } } diff --git a/common/config/main.php b/common/config/main.php index 793bdb35..be4c349b 100644 --- a/common/config/main.php +++ b/common/config/main.php @@ -34,7 +34,11 @@ 'class' => 'common\components\JWT' ], 'smsComponent' => [ - 'class' => 'common\components\SMSComponent' + 'class' => 'common\components\SMSComponent', + 'apiEndpoint' => getenv('SMS_PROVIDER_ENDPOINT') ?: null, + 'username' => getenv('SMS_PROVIDER_USERNAME') ?: null, + 'password' => getenv('SMS_PROVIDER_PASSWORD') ?: null, + 'sender' => getenv('SMS_PROVIDER_SENDER') ?: null, ], 'jira' => [ 'class' => 'common\components\JiraComponent', diff --git a/docs/setup.md b/docs/setup.md index 53550fa5..9e62f40c 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -56,4 +56,18 @@ cd console && ../yii algolia/index candidate ```bash ./yii cron/update-candidate-stats ./yii cron/update-company-stats -``` \ No newline at end of file +``` + +## Runtime Service Credentials + +### SMS Provider + +The password-reset SMS component reads provider settings from environment variables. Do not commit provider usernames, passwords, sender accounts, or private provider URLs. + +Required variables: +* `SMS_PROVIDER_ENDPOINT` - HTTPS endpoint for the SMS provider API. +* `SMS_PROVIDER_USERNAME` - provider account username. +* `SMS_PROVIDER_PASSWORD` - provider account password. +* `SMS_PROVIDER_SENDER` - approved sender name shown to recipients. + +The component fails closed when any required value is missing or when the endpoint is not HTTPS, so provider credentials are not sent over plaintext HTTP. diff --git a/scripts/check-sms-provider-hardening.py b/scripts/check-sms-provider-hardening.py new file mode 100644 index 00000000..284f7c0f --- /dev/null +++ b/scripts/check-sms-provider-hardening.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Static guard for SMS provider credential configuration.""" + +from pathlib import Path +import re +import sys + + +ROOT = Path(__file__).resolve().parents[1] +SMS_COMPONENT = ROOT / "common" / "components" / "SMSComponent.php" +MAIN_CONFIG = ROOT / "common" / "config" / "main.php" + + +def fail(message): + print(f"SMS provider hardening check failed: {message}", file=sys.stderr) + sys.exit(1) + + +component = SMS_COMPONENT.read_text() +config = MAIN_CONFIG.read_text() + +if re.search(r"\$apiEndpoint\s*=\s*['\"]https?://", component): + fail("SMSComponent must not hardcode the provider endpoint") + +for provider_key in ("UID", "p", "S"): + pattern = rf"['\"]{re.escape(provider_key)}['\"]\s*=>\s*['\"]" + if re.search(pattern, component): + fail(f"SMSComponent must not hardcode provider parameter {provider_key}") + +if "https://" not in component or "must use HTTPS" not in component: + fail("SMSComponent must enforce HTTPS transport") + +required_env = [ + "SMS_PROVIDER_ENDPOINT", + "SMS_PROVIDER_USERNAME", + "SMS_PROVIDER_PASSWORD", + "SMS_PROVIDER_SENDER", +] + +for env_name in required_env: + if f"getenv('{env_name}')" not in config: + fail(f"common config must read {env_name} from the runtime environment") + +sms_block_match = re.search( + r"'smsComponent'\s*=>\s*\[(?P.*?)^\s*\],", + config, + re.MULTILINE | re.DOTALL, +) +if not sms_block_match: + fail("common config must define the smsComponent block") + +sms_block = sms_block_match.group("body") + +for option in ("apiEndpoint", "username", "password", "sender"): + literal_pattern = rf"['\"]{option}['\"]\s*=>\s*['\"]" + if re.search(literal_pattern, sms_block): + fail(f"smsComponent option {option} must not use a committed literal") + +if re.search(r"['\"]apiEndpoint['\"]\s*=>\s*['\"]http://", sms_block, re.IGNORECASE): + fail("smsComponent must not configure a plaintext HTTP endpoint") + +print("SMS provider hardening check passed.") From 01474d6166f828b60918c8d558492225285bfa6c Mon Sep 17 00:00:00 2001 From: jamilahmadzai Date: Sat, 16 May 2026 23:43:12 +0200 Subject: [PATCH 2/2] Refine SMS provider validation --- common/components/SMSComponent.php | 11 ++++++++++- docs/setup.md | 2 +- scripts/check-sms-provider-hardening.py | 12 ++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/common/components/SMSComponent.php b/common/components/SMSComponent.php index 981b721b..9c94a8da 100644 --- a/common/components/SMSComponent.php +++ b/common/components/SMSComponent.php @@ -43,7 +43,16 @@ public function init() parent::init(); foreach (['apiEndpoint', 'username', 'password', 'sender'] as $attribute) { - if (!is_string($this->$attribute) || trim($this->$attribute) === '') { + if (!is_string($this->$attribute)) { + throw new InvalidConfigException(strtr('"{class}::{attribute}" cannot be empty.', [ + '{class}' => static::class, + '{attribute}' => '$' . $attribute + ])); + } + + $this->$attribute = trim($this->$attribute); + + if ($this->$attribute === '') { throw new InvalidConfigException(strtr('"{class}::{attribute}" cannot be empty.', [ '{class}' => static::class, '{attribute}' => '$' . $attribute diff --git a/docs/setup.md b/docs/setup.md index 9e62f40c..e77bdfdf 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -62,7 +62,7 @@ cd console && ../yii algolia/index candidate ### SMS Provider -The password-reset SMS component reads provider settings from environment variables. Do not commit provider usernames, passwords, sender accounts, or private provider URLs. +The SMS component reads provider settings from environment variables. Do not commit provider usernames, passwords, sender accounts, or private provider URLs. Required variables: * `SMS_PROVIDER_ENDPOINT` - HTTPS endpoint for the SMS provider API. diff --git a/scripts/check-sms-provider-hardening.py b/scripts/check-sms-provider-hardening.py index 284f7c0f..4157b9ff 100644 --- a/scripts/check-sms-provider-hardening.py +++ b/scripts/check-sms-provider-hardening.py @@ -12,12 +12,20 @@ def fail(message): + """Exit with a consistent check failure message.""" print(f"SMS provider hardening check failed: {message}", file=sys.stderr) sys.exit(1) -component = SMS_COMPONENT.read_text() -config = MAIN_CONFIG.read_text() +def read_required_text(path, label): + """Read a required repository file or fail with a clear path.""" + if not path.exists(): + fail(f"{label} not found at {path}") + return path.read_text() + + +component = read_required_text(SMS_COMPONENT, "SMSComponent") +config = read_required_text(MAIN_CONFIG, "common config") if re.search(r"\$apiEndpoint\s*=\s*['\"]https?://", component): fail("SMSComponent must not hardcode the provider endpoint")