diff --git a/cloudant-change-listener/function/function.js b/cloudant-change-listener/function/function.js index 9cb696833..81279f0f3 100644 --- a/cloudant-change-listener/function/function.js +++ b/cloudant-change-listener/function/function.js @@ -4,7 +4,8 @@ function main(args) { env: process.env, }; - console.log(`Return body: ${JSON.stringify(body, null, 2)}`); + // do not log the body, as it contains the process environment (which may hold credentials) + console.log(`Returning response body with ${Object.keys(args || {}).length} args and ${Object.keys(process.env).length} env vars`); return { statusCode: 200, diff --git a/cos-to-sql/utils/cos.mjs b/cos-to-sql/utils/cos.mjs index 7f04d0937..a562fffe2 100644 --- a/cos-to-sql/utils/cos.mjs +++ b/cos-to-sql/utils/cos.mjs @@ -1,6 +1,38 @@ +// COS bucket names: 3-63 chars; lowercase letters, digits, dots and hyphens; start/end with letter or digit +const BUCKET_NAME_PATTERN = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/; +// COS regions, e.g. 'eu-de', 'us-south' +const REGION_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/; + +// Builds the COS object URL from validated parts, so that user-provided values can neither change the host nor the path prefix +function buildObjectUrl(region, bucket, key) { + if (typeof region !== "string" || !REGION_PATTERN.test(region)) { + throw new Error("Invalid COS region"); + } + if (typeof bucket !== "string" || !BUCKET_NAME_PATTERN.test(bucket)) { + throw new Error("Invalid COS bucket name"); + } + if (typeof key !== "string" || key.length === 0 || key.length > 1024) { + throw new Error("Invalid COS object key"); + } + const segments = key.split("/"); + // reject path traversal; empty segments (e.g. leading '/') are not valid for a COS key either + if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) { + throw new Error("Invalid COS object key"); + } + const encodedKey = segments.map(encodeURIComponent).join("/"); + return `https://s3.direct.${region}.cloud-object-storage.appdomain.cloud/${bucket}/${encodedKey}`; +} + export async function getObjectContent(authenticator, region, bucket, key) { const fn = "getObjectContent "; const startTime = Date.now(); + + let objectUrl; + try { + objectUrl = buildObjectUrl(region, bucket, key); + } catch (err) { + return Promise.reject(err); + } console.log(`${fn} > region: '${region}', bucket: '${bucket}', key: '${key}'`); // prepare the request to download the content of a file @@ -12,10 +44,7 @@ export async function getObjectContent(authenticator, region, bucket, key) { await authenticator.authenticate(requestOptions); // perform the request - const response = await fetch( - `https://s3.direct.${region}.cloud-object-storage.appdomain.cloud/${bucket}/${key}`, - requestOptions - ); + const response = await fetch(objectUrl, requestOptions); if (response.status !== 200) { const err = new Error(`Unexpected status code: ${response.status}`); diff --git a/cos-to-sql/utils/db.mjs b/cos-to-sql/utils/db.mjs index eac186a43..75ea8f621 100644 --- a/cos-to-sql/utils/db.mjs +++ b/cos-to-sql/utils/db.mjs @@ -52,12 +52,12 @@ export async function getPgClient(secretsManager, secretId) { return Promise.resolve(_pgPool); } - console.log(`Fetching secret '${secretId}' ...`); + console.log(`Fetching the database secret ...`); // Use the Secrets Manager API to get the secret using the secret ID const res = await secretsManager.getSecret({ id: secretId, }); - console.log(`Secret '${secretId}' fetched in ${Date.now() - startTime} ms`); + console.log(`Database secret fetched in ${Date.now() - startTime} ms`); console.log( `Connecting to the DB using SM secret '${res.result.name}' (last updated: '${res.result.updated_at}') ...` diff --git a/gallery/app/app.js b/gallery/app/app.js index 7d47b8bcf..29420acdb 100644 --- a/gallery/app/app.js +++ b/gallery/app/app.js @@ -109,7 +109,7 @@ async function handleHttpReq(req, res) { return sendResponse(res, 200); }) .catch((err) => { - console.error(`Error colorizing image '${payload.imageId}'`, err); + console.error("Error colorizing image '%s'", payload.imageId, err); if (err.message.indexOf("Access is denied due to invalid credentials") > -1) { return sendResponse(res, 403, `Failed due to access permission issues`); @@ -194,7 +194,7 @@ async function handleHttpReq(req, res) { const fd = await open(`${GALLERY_PATH}/${pictureId}`); return fd.createReadStream().pipe(res); } catch (err) { - console.error(`Error streaming gallery content '${pictureId}'`, err); + console.error("Error streaming gallery content '%s'", pictureId, err); return sendResponse(res, 503, `Error streaming gallery content: ${err}`); } } diff --git a/gallery/function/cos-service.js b/gallery/function/cos-service.js index 81eeaef63..05c5d7362 100644 --- a/gallery/function/cos-service.js +++ b/gallery/function/cos-service.js @@ -35,9 +35,7 @@ class CosService { this.authenticator = new ContainerAuthenticator({ iamProfileName: config.trustedProfileName, }); - console.log( - `CosService init - region: '${this.config.cosRegion}', bucket: ${this.config.cosBucket}, trustedProfileName: '${this.config.trustedProfileName}'` - ); + console.log("CosService initialized"); } getContentTypeFromFileName(fileName) { diff --git a/thumbnail/v2/app.go b/thumbnail/v2/app.go index 4efef7d7d..6486a2099 100644 --- a/thumbnail/v2/app.go +++ b/thumbnail/v2/app.go @@ -288,7 +288,6 @@ func main() { log.Printf("BUCKET: %s\n", BucketName) log.Printf("JOB_NAME: %s\n", JobName) - log.Printf("COS APIKEY: %s\n", apiKey[:5]) log.Printf("COS SVCID: %s\n", svcID[:5]) log.Printf("HIDE_BUTTON: %v\n", HideButton) // ...