mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-06 22:13:04 +00:00
221 lines
6.8 KiB
JavaScript
221 lines
6.8 KiB
JavaScript
/*
|
|
* Copyright (C) 2024-present Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import { HttpError } from '../core/http/HttpError.js';
|
|
|
|
/**
|
|
* Small input validation utilities for driver methods. Throws HttpError(400,
|
|
* ...) on failure. Returns the value on success.
|
|
*/
|
|
|
|
/**
|
|
* Real web origins. Anything outside this list is an XSS/SSRF primitive once
|
|
* the value is consumed as `iframe.src`, `window.location`, a server-side
|
|
* fetch, etc. — `new URL()` alone happily parses `javascript:alert(1)`,
|
|
* `data:text/html,…`, `file:///etc/passwd`, and `vbscript:`.
|
|
*/
|
|
export const WEB_PROTOCOLS = Object.freeze(['http:', 'https:']);
|
|
|
|
/**
|
|
* Web origins plus the browser-extension schemes, for app origins and
|
|
* `index_url` values. Shared by AuthService.#originFromUrl,
|
|
* AppStore.createFromOrigin and AppDriver.#validateInput.
|
|
*
|
|
* Only Chrome derives the extension host from the signing key. Firefox and
|
|
* Safari generate it per install, so one extension is a distinct origin — and
|
|
* therefore a distinct app row — for each user who installs it.
|
|
*/
|
|
export const WEB_AND_EXTENSION_PROTOCOLS = Object.freeze([
|
|
...WEB_PROTOCOLS,
|
|
'chrome-extension:',
|
|
'moz-extension:',
|
|
'safari-extension:',
|
|
'safari-web-extension:',
|
|
]);
|
|
|
|
export function validateString(
|
|
value,
|
|
{ key, maxLen, regex, required = true, allowEmpty = false } = {},
|
|
) {
|
|
if (value === undefined || value === null) {
|
|
if (required)
|
|
throw new HttpError(400, `Missing \`${key}\``, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
return value;
|
|
}
|
|
if (typeof value !== 'string') {
|
|
throw new HttpError(400, `\`${key}\` must be a string`, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
}
|
|
if (!allowEmpty && value.length === 0) {
|
|
throw new HttpError(400, `\`${key}\` must not be empty`, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
}
|
|
if (maxLen && value.length > maxLen) {
|
|
throw new HttpError(
|
|
400,
|
|
`\`${key}\` must be at most ${maxLen} characters`,
|
|
{ legacyCode: 'bad_request' },
|
|
);
|
|
}
|
|
if (regex && !regex.test(value)) {
|
|
throw new HttpError(400, `\`${key}\` has an invalid format`, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function validateUrl(
|
|
value,
|
|
{
|
|
key,
|
|
maxLen = 3000,
|
|
required = true,
|
|
// http(s) by default (see WEB_PROTOCOLS); callers that need something
|
|
// exotic must opt in explicitly.
|
|
protocols = WEB_PROTOCOLS,
|
|
} = {},
|
|
) {
|
|
if (value === undefined || value === null) {
|
|
if (required)
|
|
throw new HttpError(400, `Missing \`${key}\``, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
return value;
|
|
}
|
|
validateString(value, { key, maxLen, required });
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(value);
|
|
} catch {
|
|
throw new HttpError(400, `\`${key}\` must be a valid URL`, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
}
|
|
if (!protocols.includes(parsed.protocol)) {
|
|
throw new HttpError(
|
|
400,
|
|
`\`${key}\` must use one of the following protocols: ${protocols.join(', ')}`,
|
|
{ legacyCode: 'bad_request' },
|
|
);
|
|
}
|
|
// Only "special" schemes (http:, https:, ws:, …) require an authority.
|
|
// `new URL()` accepts `chrome-extension:` and `extension:javascript:alert(1)`
|
|
// with an empty host, which slips past every host-based guard downstream.
|
|
if (!parsed.hostname) {
|
|
throw new HttpError(400, `\`${key}\` must include a host`, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function validateBool(value, { key, required = false } = {}) {
|
|
if (value === undefined || value === null) {
|
|
if (required)
|
|
throw new HttpError(400, `Missing \`${key}\``, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
return value;
|
|
}
|
|
return Boolean(value);
|
|
}
|
|
|
|
export function validateJsonObject(
|
|
value,
|
|
{ key, required = false, maxBytes } = {},
|
|
) {
|
|
if (value === undefined || value === null) {
|
|
if (required)
|
|
throw new HttpError(400, `Missing \`${key}\``, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
return value;
|
|
}
|
|
if (typeof value === 'string') {
|
|
try {
|
|
value = JSON.parse(value);
|
|
} catch {
|
|
throw new HttpError(400, `\`${key}\` must be valid JSON`, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
}
|
|
}
|
|
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
throw new HttpError(400, `\`${key}\` must be an object`, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
}
|
|
if (
|
|
maxBytes &&
|
|
Buffer.byteLength(JSON.stringify(value), 'utf8') > maxBytes
|
|
) {
|
|
throw new HttpError(
|
|
400,
|
|
`\`${key}\` must be at most ${maxBytes} bytes as JSON`,
|
|
{ legacyCode: 'bad_request' },
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function validateArrayOfStrings(
|
|
value,
|
|
{ key, required = false, maxItems, maxItemLen } = {},
|
|
) {
|
|
if (value === undefined || value === null) {
|
|
if (required)
|
|
throw new HttpError(400, `Missing \`${key}\``, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
return value;
|
|
}
|
|
if (!Array.isArray(value)) {
|
|
throw new HttpError(400, `\`${key}\` must be an array`, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
}
|
|
if (maxItems && value.length > maxItems) {
|
|
throw new HttpError(
|
|
400,
|
|
`\`${key}\` must have at most ${maxItems} entries`,
|
|
{ legacyCode: 'bad_request' },
|
|
);
|
|
}
|
|
for (let i = 0; i < value.length; i++) {
|
|
if (typeof value[i] !== 'string') {
|
|
throw new HttpError(400, `\`${key}[${i}]\` must be a string`, {
|
|
legacyCode: 'bad_request',
|
|
});
|
|
}
|
|
if (maxItemLen && value[i].length > maxItemLen) {
|
|
throw new HttpError(
|
|
400,
|
|
`\`${key}[${i}]\` must be at most ${maxItemLen} characters`,
|
|
{ legacyCode: 'bad_request' },
|
|
);
|
|
}
|
|
}
|
|
return value;
|
|
}
|