Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions client/src/Hooks/monitorHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ const useDeleteMonitor = () => {

const useUpdateMonitor = () => {
const [isLoading, setIsLoading] = useState(false);

const navigate = useNavigate();
const updateMonitor = async ({ monitor, redirect }) => {
try {
Expand All @@ -357,6 +358,7 @@ const useUpdateMonitor = () => {
expectedValue: monitor.expectedValue,
ignoreTlsErrors: monitor.ignoreTlsErrors,
jsonPath: monitor.jsonPath,
url: monitor.url,
...(monitor.type === "port" && { port: monitor.port }),
...(monitor.type === "hardware" && {
thresholds: monitor.thresholds,
Expand Down
84 changes: 46 additions & 38 deletions client/src/Pages/Infrastructure/Create/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ const CreateInfrastructureMonitor = () => {
return errorKey ? errors[errorKey] : null;
};

const pageSchema = infrastructureMonitorValidation.fork(["url"], (s) =>
isCreate ? s.required() : s.optional()
);

// Populate form fields if editing
useEffect(() => {
if (isCreate) {
Expand Down Expand Up @@ -196,7 +200,7 @@ const CreateInfrastructureMonitor = () => {
secret: infrastructureMonitor.secret,
};

const { error } = infrastructureMonitorValidation.validate(form, {
const { error } = pageSchema.validate(form, {
abortEarly: false,
});

Expand Down Expand Up @@ -234,6 +238,7 @@ const CreateInfrastructureMonitor = () => {
form = {
...(isCreate ? {} : { _id: monitorId }),
...rest,
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
description: form.name,
type: "hardware",
notifications: infrastructureMonitor.notifications,
Expand All @@ -251,20 +256,24 @@ const CreateInfrastructureMonitor = () => {
};

const onChange = (event) => {
const { value, name } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: value,
});
const { name, value } = event.target;

setInfrastructureMonitor((prev) => ({ ...prev, [name]: value }));

if (name === "url") {
const candidate = value ? `http${https ? "s" : ""}://` + value : value;

const { error } = infrastructureMonitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
const urlSchema = pageSchema.extract("url");
const { error } = urlSchema.validate(candidate, { abortEarly: false });

setErrors((prev) => ({
...prev,
url: error ? error.details[0].message : undefined,
}));
return;
}

// leave other fields exactly as they were (or no per-field validation if that’s your current behavior)
};

const handleCheckboxChange = (event) => {
Expand Down Expand Up @@ -470,31 +479,30 @@ const CreateInfrastructureMonitor = () => {
onChange={onChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
disabled={!isCreate}
/>
{isCreate && (
<FieldWrapper
label={t("infrastructureProtocol")}
labelVariant="p"
>
<ButtonGroup>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
</FieldWrapper>
)}

<FieldWrapper
label={t("infrastructureProtocol")}
labelVariant="p"
>
<ButtonGroup>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
</FieldWrapper>

<TextInput
type="text"
id="name"
Expand Down
25 changes: 16 additions & 9 deletions client/src/Validation/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,20 +367,27 @@ const infrastructureMonitorValidation = joi.object({
.string()
.trim()
.custom((value, helpers) => {
const urlRegex =
/^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i;

if (!urlRegex.test(value)) {
return helpers.error("string.invalidUrl");
if (!/^https?:\/\//i.test(value)) return helpers.error("string.uri");
try {
const u = new URL(value);
const hasPath = u.pathname && u.pathname !== "/";
const hasQuery = !!u.search;
const hasHash = !!u.hash;
if (hasPath || hasQuery || hasHash) {
return helpers.error("string.invalidUrl");
}
return value;
} catch {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Block credentials in URL and validate port range

Currently, URLs like http://user:pass@host and ports outside 1–65535 slip through. This contradicts “Only hostname (optional port)” and risks credential leakage.

Apply this diff to harden validation:

-      const hasPath = u.pathname && u.pathname !== "/";
+      const hasPath = u.pathname !== "/";
       const hasQuery = !!u.search;
       const hasHash = !!u.hash;
+      const hasCreds = !!(u.username || u.password);
+      if (hasCreds) {
+        return helpers.error("string.invalidUrl");
+      }
+      if (u.port) {
+        const portNum = Number(u.port);
+        if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
+          return helpers.error("string.invalidUrl");
+        }
+      }
       if (hasPath || hasQuery || hasHash) {
         return helpers.error("string.invalidUrl");
       }
-      return value;
+      // Normalize to canonical form: scheme://host[:port]
+      return `${u.protocol}//${u.host}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const u = new URL(value);
const hasPath = u.pathname && u.pathname !== "/";
const hasQuery = !!u.search;
const hasHash = !!u.hash;
if (hasPath || hasQuery || hasHash) {
return helpers.error("string.invalidUrl");
}
return value;
} catch {
const u = new URL(value);
const hasPath = u.pathname !== "/";
const hasQuery = !!u.search;
const hasHash = !!u.hash;
const hasCreds = !!(u.username || u.password);
if (hasCreds) {
return helpers.error("string.invalidUrl");
}
if (u.port) {
const portNum = Number(u.port);
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
return helpers.error("string.invalidUrl");
}
}
if (hasPath || hasQuery || hasHash) {
return helpers.error("string.invalidUrl");
}
// Normalize to canonical form: scheme://host[:port]
return `${u.protocol}//${u.host}`;
} catch {
🤖 Prompt for AI Agents
In client/src/Validation/validation.js around lines 372 to 380, the URL
validation does not block URLs containing user credentials or ports outside the
valid range 1–65535, which violates the requirement to allow only hostname and
optional port. Update the validation logic to check if the URL includes username
or password and reject it, and also verify that if a port is specified, it falls
within the valid range; if not, return the validation error "string.invalidUrl".

return helpers.error("string.uri");
}

return value;
})
.messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
"string.invalidUrl": "Please enter a valid URL with optional port",
"string.uri": "Please enter a valid URL starting with http:// or https://",
"string.invalidUrl":
"Only hostname (optional port) is allowed — no path, query, or fragment.",
}),
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm client/server validation parity (paths, creds, ports)

Ensure server-side schemas match these client constraints to prevent UX mismatches (client rejects while server accepts, or vice versa).


🏁 Script executed:

#!/bin/bash
# Verify server-side URL constraints mirror client rules:
# - http/https only
# - no path/query/fragment
# - no credentials
# - port in 1..65535

# Locate joi validation files
fd -i -a 'joi.js'

# Inspect monitor body validations
rg -n -A5 -B5 'createMonitorBodyValidation|editMonitorBodyValidation'

# Inspect URL rules and custom validators
rg -n -A6 -B3 'url.*joi|Joi.*uri|Joi.*custom|credentials|username|password|pathname|search|hash|port'

# As a fallback, scan for any URI validations that might allow paths
rg -n -A3 -B3 'uri\('

Length of output: 295827


Align server-side URL validation with client constraints

The server currently uses

.string()
.uri({ scheme: ["http", "https"] })

which only enforces the protocol. It still allows paths, query strings, fragments, and credentials—unlike the client’s custom validator that forbids them. This will lead to confusing UX where the client rejects a URL the server accepts (or vice versa).

Please update the schema in server/src/validation/joi.js:

  • createMonitorBodyValidation.url (around line 158)
  • editMonitorBodyValidation.url (around line 188)

to mirror the client logic by disallowing:
• path (u.pathname !== "/")
• query (u.search)
• fragment (u.hash)
• credentials (u.username/u.password)
and still enforcing a valid port (1–65535).

You can use Joi’s URI options (e.g. allowQuery: false, allowFragments: false, allowCredentials: false) or plug in a custom validator akin to the client’s new URL(value) check.

Let’s keep Canada and the U.S. arguing over hockey, not URL formats! 😉

🤖 Prompt for AI Agents
In server/src/validation/joi.js around lines 158 and 188, update the
createMonitorBodyValidation.url and editMonitorBodyValidation.url schemas to
match the client-side URL validation by disallowing paths, queries, fragments,
and credentials. Use Joi's URI options such as allowQuery: false,
allowFragments: false, allowCredentials: false to enforce these restrictions,
and ensure the port is valid (1-65535). Alternatively, implement a custom
validator similar to the client’s new URL(value) check that verifies pathname is
"/", no search or hash exists, and username and password are empty.


name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
Expand Down
10 changes: 9 additions & 1 deletion server/src/validation/joi.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,11 @@ const createMonitorBodyValidation = joi.object({
name: joi.string().required(),
description: joi.string().required(),
type: joi.string().required(),
url: joi.string().required(),
url: joi
.string()
.trim()
.uri({ scheme: ["http", "https"] })
.required(),
ignoreTlsErrors: joi.boolean().default(false),
port: joi.number(),
isActive: joi.boolean(),
Expand All @@ -181,6 +185,10 @@ const createMonitorsBodyValidation = joi.array().items(
);

const editMonitorBodyValidation = joi.object({
url: joi
.string()
.uri({ scheme: ["http", "https"] })
.optional(),
name: joi.string(),
description: joi.string(),
interval: joi.number(),
Expand Down