Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security Vulnerability in Apps Script API #156

Open
AQIB-NAWAB opened this issue Mar 22, 2025 · 18 comments
Open

Security Vulnerability in Apps Script API #156

AQIB-NAWAB opened this issue Mar 22, 2025 · 18 comments
Labels
Status: Triage This is the initial status for an issue that requires triage.

Comments

@AQIB-NAWAB
Copy link
Contributor

AQIB-NAWAB commented Mar 22, 2025

Right now, anyone who knows the API URL can send a request and add data to our Google Sheet. This means unauthorized users could add fake or incorrect data just by making a POST request.

Why is this happening?

The API doesn’t have any security checks in place. There’s nothing stopping someone from sending data, even if they’re not supposed to.

Solution

  1. We should require a SECRET_KEY with every request. Here’s how it will work:
  2. The frontend will send a SECRET_KEY along with the request in body.
  3. The Apps Script function will check if the key matches the one stored on the backend.
  4. If the key is correct, the request is processed. If not, it gets rejected immediately.

Here is the code to solve the problem

const SECRET_KEY = "OUR_SECRET_KEY";
const postData = JSON.parse(e.postData.contents);
const receivedSecret = postData.secret_key;

// Check if the secret key is provided and matches
    if (!receivedSecret || receivedSecret !== SECRET_KEY) {
      return ContentService.createTextOutput(
        JSON.stringify({ status: "error", message: "Unauthorized access" })
      ).setMimeType(ContentService.MimeType.JSON);
    }

Here is the video of current behavior

Screencast.from.2025-03-22.19-41-24.webm
@AQIB-NAWAB AQIB-NAWAB changed the title Issue Description: Security Vulnerability in Apps Script API Security Vulnerability in Apps Script API Mar 22, 2025
@AQIB-NAWAB
Copy link
Contributor Author

@JeelRajodiya @benjagm please assign this issue to me.

@AQIB-NAWAB
Copy link
Contributor Author

AQIB-NAWAB commented Mar 22, 2025

For local development for others users we can provide them SECRET_KEY for testing purpose sheet while for production we can use the main one.

In this way it follows the open source structure.

Thanks

@JeelRajodiya
Copy link
Member

@AQIB-NAWAB If we store the secret key in the frontend, don't you think anyone can view our repo and see that secret key as well?

@AQIB-NAWAB
Copy link
Contributor Author

We will store the secret key in env file

@AQIB-NAWAB
Copy link
Contributor Author

AQIB-NAWAB commented Mar 22, 2025

For development purpose we provide only TEST_SECRET_KEY.
As spreadsheet have many sheets we consider one sheet as development sheet and use it for just development purpose while main sheet remains for production

@AQIB-NAWAB
Copy link
Contributor Author

AQIB-NAWAB commented Mar 22, 2025

Here is sample code which follows the development as well as production guidelines

const SECRET_KEY = "OUR_SECRET_KEY";
const postData = JSON.parse(e.postData.contents);
const receivedSecret = postData.secret_key;

// Check if the secret key is provided and matches
    if (!receivedSecret) {
      return ContentService.createTextOutput(
        JSON.stringify({ status: "error", message: "Unauthorized access , Please provide the SECRET KEY" })
      ).setMimeType(ContentService.MimeType.JSON);
    }

    let sheet=null;

    if(receivedSecret=="TEST_SECRET_KEY"){
      sheet = SpreadsheetApp.openById("TEST_SHEET_ID");
    }else if(receivedSecret==SECRET_KEY){
      sheet = SpreadsheetApp.openById("SHEET_ID");
    }else{
      return ContentService.createTextOutput(
        JSON.stringify({ status: "error", message: "Unauthorized access , Please provide the correct SECRET KEY" })
      ).setMimeType(ContentService.MimeType.JSON);
    }

@JeelRajodiya
Copy link
Member

JeelRajodiya commented Mar 22, 2025

We will store the secret key in env file

Our project is client side only, anything we store in the env file will eventually reflect in the build code. I am aware of this issue, but it is not likely anyone will spam the app script api. if we want to solve this we need a better solution rather than sending a secret from the client (you can know the secret key if you inspect the tour code in network tab). I am not sure how storing a key in client can make it more secure, if you have any example where a project is storing a key in the client to the server to verify the request was sent by the dedicated client, please share.

@AQIB-NAWAB
Copy link
Contributor Author

AQIB-NAWAB commented Mar 22, 2025

We will store the secret key in env file

Our project is client side only, anything we store in the env file will eventually reflect in the build code. I am aware of this issue, but it is not likely anyone will spam the app script api. if we want to solve this we need a better solution rather than sending a secret from the client (you can know the secret key if you inspect the tour code in network tab). I am not sure how storing a key in client can make it more secure, if you have any example where a project is storing a key in the client to the server to verify the request was sent by the dedicated client, please share.

We can enhance our security by sending the secret key through request headers, similar to how cookies are used for authentication in many projects. During deployment, our environment variables will be injected into the application, ensuring that these keys do not appear in the build code. This approach helps to keep our secret keys secure while still allowing the necessary authentication between the client and the server.

@AQIB-NAWAB
Copy link
Contributor Author

AQIB-NAWAB commented Mar 22, 2025

There is another approach to solve this problem is validating the domain of client for the request. If it is from tour.json.com
then we can use production one else development one.

@JeelRajodiya
Copy link
Member

We will store the secret key in env file

Our project is client side only, anything we store in the env file will eventually reflect in the build code. I am aware of this issue, but it is not likely anyone will spam the app script api. if we want to solve this we need a better solution rather than sending a secret from the client (you can know the secret key if you inspect the tour code in network tab). I am not sure how storing a key in client can make it more secure, if you have any example where a project is storing a key in the client to the server to verify the request was sent by the dedicated client, please share.

We can enhance our security by sending the secret key through request headers, similar to how cookies are used for authentication in many projects. During deployment, our environment variables will be injected into the application, ensuring that these keys do not appear in the build code. This approach helps to keep our secret keys secure while still allowing the necessary authentication between the client and the server.

"During deployment, our environment variables will be injected into the application, ensuring that these keys do not appear in the build code" Can you elaborate this more please, I am unable to understand what is the difference between application and build code?

@JeelRajodiya
Copy link
Member

There is another approach to solve this problem is validating the domain of client for the request. If it is from tour.json.com then we can use production one else development one.

This sounds good, how can we validate the domain of the client request?

@JeelRajodiya JeelRajodiya added the Status: Triage This is the initial status for an issue that requires triage. label Mar 22, 2025
@AQIB-NAWAB
Copy link
Contributor Author

AQIB-NAWAB commented Mar 22, 2025

There is another approach to solve this problem is validating the domain of client for the request. If it is from tour.json.com then we can use production one else development one.

This sounds good, how can we validate the domain of the client request?

Here is function that return sheet according to domain of client side. we can extract the domain from the headers.Referer.


function getSpreadsheetByDomain(e) {
  // Get the referrer URL from the request headers
  var headers = e.headers || {};
  var referer = headers.Referer || headers.referer || '';
  
  // Extract the domain from the referer
  var domain = '';
  if (referer) {
    try {
      var urlObj = new URL(referer);
      domain = urlObj.hostname;
    } catch (error) {
      console.log('Error parsing referer URL: ' + error.message);
    }
  }
  
  // Define your spreadsheet IDs
  const PRODUCTION_SPREADSHEET_ID = 'prod_id';
  const DEVELOPMENT_SPREADSHEET_ID = 'dev_id';
  
  // Choose the appropriate spreadsheet based on domain
  if (domain === 'tour.json.com') {
    // Use production spreadsheet
    return SpreadsheetApp.openById(PRODUCTION_SPREADSHEET_ID);
  } else {
    // Use development spreadsheet for all other domains
    return SpreadsheetApp.openById(DEVELOPMENT_SPREADSHEET_ID);
  }
}

Another approach is that we can have our own backend which contains all the secrets and our client side made request to backend and forward it google app script.

@AQIB-NAWAB
Copy link
Contributor Author

We will store the secret key in env file

Our project is client side only, anything we store in the env file will eventually reflect in the build code. I am aware of this issue, but it is not likely anyone will spam the app script api. if we want to solve this we need a better solution rather than sending a secret from the client (you can know the secret key if you inspect the tour code in network tab). I am not sure how storing a key in client can make it more secure, if you have any example where a project is storing a key in the client to the server to verify the request was sent by the dedicated client, please share.

We can enhance our security by sending the secret key through request headers, similar to how cookies are used for authentication in many projects. During deployment, our environment variables will be injected into the application, ensuring that these keys do not appear in the build code. This approach helps to keep our secret keys secure while still allowing the necessary authentication between the client and the server.

"During deployment, our environment variables will be injected into the application, ensuring that these keys do not appear in the build code" Can you elaborate this more please, I am unable to understand what is the difference between application and build code?

It is like saving envs in deployment platforms as secrets. For example in Vercel , Cloudflare.But we still ending up exposing the SECRET from client side.

@JeelRajodiya
Copy link
Member

We will store the secret key in env file

Our project is client side only, anything we store in the env file will eventually reflect in the build code. I am aware of this issue, but it is not likely anyone will spam the app script api. if we want to solve this we need a better solution rather than sending a secret from the client (you can know the secret key if you inspect the tour code in network tab). I am not sure how storing a key in client can make it more secure, if you have any example where a project is storing a key in the client to the server to verify the request was sent by the dedicated client, please share.

We can enhance our security by sending the secret key through request headers, similar to how cookies are used for authentication in many projects. During deployment, our environment variables will be injected into the application, ensuring that these keys do not appear in the build code. This approach helps to keep our secret keys secure while still allowing the necessary authentication between the client and the server.

"During deployment, our environment variables will be injected into the application, ensuring that these keys do not appear in the build code" Can you elaborate this more please, I am unable to understand what is the difference between application and build code?

It is like saving envs in deployment platforms as secrets. For example in Vercel , Cloudflare.But we still ending up exposing the SECRET from client side.

exactly, we can't put any secrets in our project since it is client side only. Vercel keeps exposes those secrets to the /api folder only, which is for backed - and we don't have backed in our project.

@JeelRajodiya
Copy link
Member

There is another approach to solve this problem is validating the domain of client for the request. If it is from tour.json.com then we can use production one else development one.

This sounds good, how can we validate the domain of the client request?

Here is function that return sheet according to domain of client side. we can extract the domain from the headers.Referer.


function getSpreadsheetByDomain(e) {
  // Get the referrer URL from the request headers
  var headers = e.headers || {};
  var referer = headers.Referer || headers.referer || '';
  
  // Extract the domain from the referer
  var domain = '';
  if (referer) {
    try {
      var urlObj = new URL(referer);
      domain = urlObj.hostname;
    } catch (error) {
      console.log('Error parsing referer URL: ' + error.message);
    }
  }
  
  // Define your spreadsheet IDs
  const PRODUCTION_SPREADSHEET_ID = 'prod_id';
  const DEVELOPMENT_SPREADSHEET_ID = 'dev_id';
  
  // Choose the appropriate spreadsheet based on domain
  if (domain === 'tour.json.com') {
    // Use production spreadsheet
    return SpreadsheetApp.openById(PRODUCTION_SPREADSHEET_ID);
  } else {
    // Use development spreadsheet for all other domains
    return SpreadsheetApp.openById(DEVELOPMENT_SPREADSHEET_ID);
  }
}

Another approach is that we can have our own backend which contains all the secrets and our client side made request to backend and forward it google app script.

Anyone can put custom headers and pretend to be from our domain, Can we think of a better approach?

@AQIB-NAWAB
Copy link
Contributor Author

There is another approach to solve this problem is validating the domain of client for the request. If it is from tour.json.com then we can use production one else development one.

This sounds good, how can we validate the domain of the client request?

Here is function that return sheet according to domain of client side. we can extract the domain from the headers.Referer.


function getSpreadsheetByDomain(e) {
  // Get the referrer URL from the request headers
  var headers = e.headers || {};
  var referer = headers.Referer || headers.referer || '';
  
  // Extract the domain from the referer
  var domain = '';
  if (referer) {
    try {
      var urlObj = new URL(referer);
      domain = urlObj.hostname;
    } catch (error) {
      console.log('Error parsing referer URL: ' + error.message);
    }
  }
  
  // Define your spreadsheet IDs
  const PRODUCTION_SPREADSHEET_ID = 'prod_id';
  const DEVELOPMENT_SPREADSHEET_ID = 'dev_id';
  
  // Choose the appropriate spreadsheet based on domain
  if (domain === 'tour.json.com') {
    // Use production spreadsheet
    return SpreadsheetApp.openById(PRODUCTION_SPREADSHEET_ID);
  } else {
    // Use development spreadsheet for all other domains
    return SpreadsheetApp.openById(DEVELOPMENT_SPREADSHEET_ID);
  }
}

Another approach is that we can have our own backend which contains all the secrets and our client side made request to backend and forward it google app script.

Anyone can put custom headers and pretend to be from our domain, Can we think of a better approach?

How about IP address or using the JWT token same as we do for authentication.

@AQIB-NAWAB
Copy link
Contributor Author

AQIB-NAWAB commented Mar 26, 2025

@JeelRajodiya @benjagm I have another approach.

We can make an API request from the frontend using Next.js API routes. The request will include the user's name and email:

POST
tour.json-schema.com/api/assign
Body: { name, email }

In the API route, we'll call another backend endpoint while keeping the secret key stored in environment variables. This way, all logic is handled on our custom backend server, and secrets remain secure since they won’t be exposed in network requests.

@JeelRajodiya
Copy link
Member

@JeelRajodiya @benjagm I have another approach.

We can make an API request from the frontend using Next.js API routes. The request will include the user's name and email:

POST tour.json-schema.com/api/assign Body: { name, email }

In the API route, we'll call another backend endpoint while keeping the secret key stored in environment variables. This way, all logic is handled on our custom backend server, and secrets remain secure since they won’t be exposed in network requests.

Our project is hosted on github pages, we can't implement backend or any api endpoints.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Triage This is the initial status for an issue that requires triage.
Projects
None yet
Development

No branches or pull requests

2 participants