4

In Google Sheets, there is a script that uses "UrlFetchApp" to obtain information from an external API that requires an API key to be included in each call.

The sheet has a number of editors but only the owner should be able to see the API key, so storing the key in the script itself or using the PropertiesService is not an option.

Would the following solution keep sheet editors from seeing the key?

  1. Create a new stand-alone Apps Script project.

  2. In the stand-alone script, create the following function:

     function fetchData(idFromSheetScript) {
       var secret = '/abc123';
       var id = idFromSheetScript;
       var uri = 'https://.../'; 
       var url = uri+id+secret;
       var data = UrlFetchApp.fetch(url);
       return data;
     }
    
  3. Deploy the stand-alone script as a Library. Do not share the project with anyone.

  4. In the Google Sheets-bound script, import the Library and use fetchData() function from the Library.

     var response = fetchData('10');
    

Would the editors of the sheet where the Library is imported be able to see or obtain (through logging or otherwise) the "secret" variable in the Library or would they only be able to see the function's returned variable?

TheMaster
  • 45,448
  • 6
  • 62
  • 85
que
  • 121
  • 1
  • 8
  • Strongly related: https://stackoverflow.com/questions/31993969/how-can-i-view-the-source-of-a-google-apps-script-library?rq=1 – Kos Dec 26 '21 at 22:05
  • @Kos when an editor tries to change the script that uses the library to which the user has no access, the following error is displayed: "You do not have access to library , used by your script, or it has been deleted." However, the API call still works due to the installable onEdit() trigger, it appears. How can the editor get to the Library source code in this scenario? – que Dec 27 '21 at 16:25
  • this can work in a simple scenario, but largely, code executed under user specific account; if code includes any secrets, they are automatically available to user account, like it or not. From this perspective it's worth to try limit your API key scope. – Kos Dec 27 '21 at 16:37
  • @Kos About `if code includes any secrets, they are automatically available to user account, like it or not`. I just want to make sure I'm not missing something. In the scenario of installable edit trigger described in my answer, do you see any possibility of getting the key by editors, who don't have access to library? – TheMaster Dec 28 '21 at 07:03
  • 1
    Review requested in official [chat room](https://chat.stackoverflow.com/rooms/217630/google-apps-script-chat-community) – TheMaster Dec 28 '21 at 07:09
  • "PropertiesService is not an option" - actually it is. You can make what gets the key a function that accepts an instance of `Spreadsheet` and check for `getOwner().getEmail()` equality to `getEffectiveUser().getEmail()`. Return the prop on success and empty string on failure. P.S. Yes, it does not stop a malicious editor from supplying a bogus spreadsheet instance – Oleg Valter is with Ukraine Dec 28 '21 at 15:03
  • Alternatively, you can obtain the instance "internally" in the library itself, that should deal with the potentially malicious actors supplying bogus objects. – Oleg Valter is with Ukraine Dec 28 '21 at 15:07
  • It does not really make sense to create a *library* if its consumers are not able to modify its code in any way given the nature of JS, the source is always available. You will encounter the same issue with Node.js dependencies, for example (to be fair, it *does* now have a Policies module for mitigating that) – Oleg Valter is with Ukraine Dec 28 '21 at 15:17
  • @OlegValter, with the library function "fetchData(idFromSheetScript)" described in the original post, can a sheet editor write code to expose the "secret" variable from within the library if the editor does not have access to the library project? – que Dec 28 '21 at 19:16
  • @que you mean, like, as in log the value of the stored property if you do not expose it? Should not be able to - `ScriptProperties` is, indeed, a [non-shared resource](https://developers.google.com/apps-script/guides/libraries#resource_scoping), so unless you expose them via the library, consumers of the library will not be able to see the secret. Quick [demo](https://tsplay.dev/m3aljw) of what I mean (TypeScript playground) – Oleg Valter is with Ukraine Dec 28 '21 at 19:21
  • @que ah, that - well, yes, as TheMaster's answer correctly indicates, you *need* to share the library code with at least read (view) access with anyone, which means that anyone who knows about your library will be able to see the hardcoded value. Methinks your best bet is script properties (with some caching on top to reduce the number of reads to it, the quota's not infinite) – Oleg Valter is with Ukraine Dec 28 '21 at 19:39
  • @OlegValter, the Library is not shared with any other user. The call to the Library function works because it is set up with an installable trigger, which does have access to the library. My question is: how can the editor, who does not have access to the library, read the source code of the library? – que Dec 28 '21 at 20:15
  • 1
    @TheMaster no, it does not. Because the editor does not have access to the library, they see an error when they try to edit the installable trigger function that uses the library. It appears that the editor can remove the library altogether from the script, which is a different problem, but not make any changes to the code that calls the library. – que Dec 28 '21 at 20:17
  • Great. It might still be possible with [tag:google-apps-script-api] even if not possible manually. – TheMaster Dec 28 '21 at 20:35
  • @que if you use an installable trigger set to a specific version (as in TheMaster's answer) that is created by the owner, then they should not be able to. I am not sure why would you want to hardcode the key regardless of that, though - it is a problem waiting to happen, IMO. – Oleg Valter is with Ukraine Dec 28 '21 at 20:38
  • @TheMaster thank you, it's good to know that an editor might be able to see the library source code via the google-apps-script-api even if they do not have access to the library project via the UI. – que Dec 28 '21 at 21:05
  • @OlegValter when setting up the installable trigger, I now see only HEAD; version 1 is not there. Looks like I need to deploy it first, but deploy as what? But more importantly, how can I avoid hard coding it? If I put it in the ScriptProperties of the library, would it make it not reachable by an editor of the sheet that uses the library? Or is the library not necessary at that point? – que Dec 28 '21 at 21:10
  • @OlegValter What does it mean in [Properties Service](https://www.example.com) that for Script Properties "Data shared among: All users of a script, add-on, or web app?" What does that mean in relation to being non-shared? – que Dec 28 '21 at 21:27
  • @que I suggest you check the link :) Link aside, it means "scope" and is not related to the sharing of resources (see the table from the link I shared above). What you found regards the difference between script, user, and document properties - the type determines what another user/document will be able to read from the store (i.e. script properties are the same for all users, user props are per-user [thus you can save different values under the same key], and document ones are also scoped to the container document) – Oleg Valter is with Ukraine Dec 28 '21 at 21:33
  • @OlegValter thank you for the clarification. Thank you also for the TypeScript demo. Do I still need to add to that the getOwner().getEmail() equality to getEffectiveUser().getEmail() part or is that not related? – que Dec 28 '21 at 21:37
  • @que NP - I assume the call is happening in the installable trigger that the owner is setting up? I think it is not really needed then, but if you want to go all-in: https://tsplay.dev/wg609W – Oleg Valter is with Ukraine Dec 28 '21 at 21:47
  • 1
    @que just deploy as a library via "new deployment", if memory serves me right, this should be enough. You are correct that if you save something in the script properties of the library, the value will not be accessible from the including script (unless you specifically expose it, of course). Caution advised, though, to ensure you haven't left any way to expose the properties somewhere else in the codebase, but I am sure you are aware of that – Oleg Valter is with Ukraine Dec 28 '21 at 21:52
  • @OlegValter thank you; to summarize: 1. Stand-alone script with API secret in the ScriptProperties and UrlFetchApp.fetch() in a function;deployed as a library and not shares with any user. 2. The same user who deployed the library in step 1 above creates a Google Sheets with a bound script. The script imports the library from step 1. 3. The script is deployed as a Library. 4. A function in the script calls the function from the library and passes a value from the sheet as a parameter. 4. The function is set up with an onEdit installable trigger restricted to version 1. – que Dec 28 '21 at 22:02
  • @OlegValter outcome: an editor of the sheet will be able to change values in sheet cells and see and modify the script in the script editor. However, any changes to the function that is used in the installable trigger will not be executed with the permissions of the installable trigger since the installable trigger points to version 1 only. Any changes to the function in the bound script will be ignored by the trigger whether the script is redeployed as version 2 or not redeployed at all. Did I understand correctly? – que Dec 28 '21 at 22:07
  • @OlegValter Can you help me understand the "if (source.getOwner()?.getEmail() !== Session.getEffectiveUser().getEmail())" in the TypeScript example? Under what circumstances would this not evaluate to true? In other words, in what cases would the Session.getEffectiveUser() be different from the source.getOwner()? – que Dec 28 '21 at 23:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/240524/discussion-between-oleg-valter-and-que). – Oleg Valter is with Ukraine Dec 29 '21 at 08:55

2 Answers2

2
  • There are problems with your approach:

    • The library needs to be shared with atleast "view level" access to the end user, otherwise it won't function. So, point 3 is infeasible.

    • The library source code of library with identifier MyLibrary can also be retrieved with

      console.log(MyLibrary.fetchData.toString());
      //where fetchData is one of the function names in MyLibrary
      
  • Installable edit trigger:

    • These triggers run under the authority of the user who created the trigger. So, libraries can be shared with one user/dummy Google account, and have that user install a trigger, while limiting library source code access to every other editor.

    • However, If there's a editor with malicious intentions, they'd simply edit the onEdit() function to:

      function onEditInstalled(){
        SpreadsheetApp.getActiveRange().setValue(MyLibrary.fetchData.toString())
      }
      

      and edit something to get the source+api key. But they can do more nefarious things to the user who created the trigger, if installable trigger had more permissions(like access to Gmail or Drive) and if set to always run at the latest deployment.

    • The above maybe avoided by setting the trigger to always run at a predetermined version and not at HEAD/latest version. This can be changed when setting up triggers in apps script dashboard user interface. To create a version, you can create a dummy library/webapp deployment or use the api. This protects the code against any modifications, because a version is like a 'snapshot' of current code. Once a trigger is set to execute at a certain version, it cannot be changed by other editors. There is also currently no way to modify a existing version by the owner or a editor. Versions are immutable.

  • Since script properties are not shared between the library and the including script, you may use that to hide the api key.

  • Another option is to deploy the standalone script as a web app with access: anyone including anonymous, but use your authentication mechanism using identity tokens( ScriptApp.getIdentityToken()). But you need to build a proper validation mechanism on the web app side for the id token.

    Note that all these workarounds are not pen tested for security, but provided as a concept based on experience. Security is relative. Criticisms are welcome.

TheMaster
  • 45,448
  • 6
  • 62
  • 85
  • Note that all these workarounds are not pen tested for security, but provided as a concept. Criticisms are welcome. – TheMaster Dec 26 '21 at 18:30
  • For testing, I have shared the Google Sheet with another account and gave that account Viewer access to the project containing the Library. I then tried to run "Logger.log(MyLibrary.fetchData) and it returned null. Is that unexpected? Was "MyLibrary.fetchData" supposed to return the entire code of the library? – que Dec 26 '21 at 19:20
  • @que What did you name the library as? `MyLibrary` is a default name. – TheMaster Dec 26 '21 at 19:45
  • the library is called APICall, but never mind; it was a bad question. If the user has Reviewer access to the project, they can go directly to the project and see the Library script there. – que Dec 26 '21 at 20:04
  • this is the interesting part: I removed the user's Viewer access to the project containing the Library. The sheet automation still works. The user has Edit access to the sheet and enters an id in a cell; the sheet's script has an installable onEdit() trigger set up by the owner. After the user enters the id, the sheet updates automatically based on the API script as intended. When the user goes to the script editor and runs Logger.log(MyLibrary.fetchData), there is an error: "We're sorry, a server error occurred while reading from storage. Error code NOT_FOUND." – que Dec 26 '21 at 20:16
  • When the owner runs the same script with Logger.log(MyLibrary.fetchData), they do see the Library function in its entirety. When the user tries to edit the script, there is an error message: "You do not have access to library (Project ID) used by your script, or it has been deleted. But sheet functionality is still in place...It appears that Viewer access to the Library is not needed for the end user. Perhaps due to the installable onEdit() trigger? – que Dec 26 '21 at 20:17
  • @que As installable triggers run as the user who created the trigger, it has access to the library. If I am a editor with malicious intentions, I'd simply edit the ``onEdit()`` function, `function onEditInstalled(){SpreadsheetApp.getActiveRange().setValue(MyLibrary.fetchData.toString())}` and edit something to get the source+api key. But I can do more nefarious things to the owner, if installable trigger had more permissions(like access to Gmail or Drive) and set to always run at the latest deployment. – TheMaster Dec 26 '21 at 20:31
  • thank you; that makes sense. So the next step to try would be to put the secret in the script properties. Would it still be protected even if the editor changed the onEdit() installable trigger? – que Dec 26 '21 at 20:48
  • @que No. It wouldn't be protected. But I think you can set installable trigger at the specific version and not the head, when setting up the triggers through the user interface. Once set as a specific version, changes won't matter. Again, I've pen tested none of this. So do your own testing. – TheMaster Dec 26 '21 at 22:00
  • thank you for your thoughts. It remains surprising that there isn't an official way to handle credentials securely in Google Apps Script. – que Dec 26 '21 at 22:17
  • @que The official way is properties service. The official line would be: "If you trust people enough to share edit privileges, you can trust the editors with secrets". You can make a feature request though: See [tag info page](https://stackoverflow.com/tags/google-apps-script/info) for free resources and more details. Also consider accepting the answer by clicking the checkmark on the left of this post. – TheMaster Dec 26 '21 at 22:28
  • there are users who need to be able to enter information in a particular cell of the sheet but not have access to API keys or to alter the script itself. Does the web app option solve these issues? – que Dec 26 '21 at 22:55
  • the part I am not clear on is that when an editor tries to change the onEdit() function that uses the library to which the user has no access, the following error is displayed: "You do not have access to library , used by your script, or it has been deleted." However, the API call still works due to the installable onEdit() trigger. – que Dec 26 '21 at 23:10
  • 1
    In the current state, it looks more like an FR than something that can be achieved without any workarounds. As @TheMaster refers, you can go [here](https://issuetracker.google.com/issues/new?component=191640&template=824113), to ask Google to include it as a new feature. From what I see in the [collaboration documentation](https://developers.google.com/apps-script/guides/collaborating), there is not a big difference in terms of editing and publishing between owner and editor permissions, which makes it difficult to hide an API key from them. – Emel Dec 27 '21 at 16:20
  • 1
    @que As explained in my previous comment, installable triggers run as the user. So if user has library permissions, it'll work. Also, web app would help imo. – TheMaster Dec 27 '21 at 17:33
  • @TheMaster, yes, but there was also a concern that an editor could alter the onEdit() function to see the library source code even though the editor does not have access to the library project. I am trying to figure out if that is actually possible for the editor. – que Dec 27 '21 at 18:01
  • @que edited my answer. As long as you don't set up the trigger at head version, you should be good. – TheMaster Dec 27 '21 at 18:38
  • 1
    @TheMaster my main takeaway is that there is a bigger concern than the API secret in the library. Thank you for pointing out that a sheet editor can edit the function used in the installable trigger to cause much wider damage as it is not possible to limit the scope of the installable trigger to that particular spreadsheet. Installable triggers require access to all files... – que Dec 28 '21 at 19:15
  • @que As explained, set the installable trigger to a run at a specific version. In theory, no edits by any editor would matter as the code will run at a fixed version. – TheMaster Dec 28 '21 at 19:36
  • @TheMaster the issue at this point is not the editor changing the call to the library (that seems to be prevented by their not having access to the library project at all); the main issue for me is that an installable trigger requires access to all files, which an editor could exploit in numerous ways not related to the secret, as you had pointed out. – que Dec 28 '21 at 20:20
  • 1
    @que Even that *maybe* prevented by setting a specific version as noted in point 2.3 of my answer. – TheMaster Dec 28 '21 at 20:33
  • @TheMaster thank you; I now see the distinction between the version of the library and the version of the installable trigger. When adding the library, I see version 1 and HEAD. But when adding the installable trigger, I only see HEAD. How do I choose version 1 when installing the trigger? – que Dec 28 '21 at 20:59
  • 1
    @que I see Oleg already answered that. I also did mention it in my answer, if you read it carefully. – TheMaster Dec 28 '21 at 22:16
  • @TheMaster thank you for drawing my attention to it in the answer; sorry I missed that. – que Dec 28 '21 at 22:28
  • @TheMaster My testing thus far confirms the method you proposed. The editor can view the script but not change the version number of the imported library. In addition, the editor can see the installable trigger set up by the owner but not edit it. The editor can create a new installable trigger using another version but the new installable trigger requires the editor to provide authorization to the editor's own account, not the owner's. Thank you again! – que Dec 28 '21 at 23:43
  • @que Even without the library, a editor can change/ edit the script, but the installable trigger will always run at a version which the owner initially set it up. – TheMaster Dec 29 '21 at 08:20
  • @TheMaster, yes, the editor can edit the script. My question is whether the editor could use the script to access any other information in the document owner's account (drive, gmail, etc.). In other words, does the authorization go with the installable trigger, or the script, or both. Let's say the owner runs the script without an installable trigger and gives authorization to all files in the account. The the editor later changes the script and enters an id in SpreadsheetApp.openById to access other files in the owner's drive. Would they be able to? – que Dec 29 '21 at 19:18
  • 1
    @que no, fortunately not - when somebody runs a function that requires their authorization, it is run with their authorization, meaning the editor will only have access to their files and those that are shared with them. The issue TheMaster mentioned becomes a security problem only if the code is run by someone else with the authorization grants made by not them. The usual suspects are webapps deployed "as me" and installable triggers – Oleg Valter is with Ukraine Dec 29 '21 at 21:26
  • @OlegValter Thank you for the clarification. That's good to know, and a relief! – que Dec 29 '21 at 22:07
  • @que I can confirm with Oleg and am comfortable with this set up myself. But you can never be too careful. Check what you learnt and test everything. Always keep an eye for loop holes. Security is relative. – TheMaster Dec 29 '21 at 22:35
  • @TheMaster Yes, that makes sense. I learned a lot from this exchange, thank you. I have been doing testing, and so far, I am not seeing issues. One area I haven't tried is connecting through the API. You mentioned earlier that there might be ways through the API that do not exist via the UI. Is there anything in particular that causes concern there? – que Dec 29 '21 at 22:45
  • @que I don't think there's any in this case. – TheMaster Dec 29 '21 at 22:51
1

As @TheMaster mentioned, mechanism of libraries is not intended to be out of users line of sight, based on their shared nature.

Once library included in project and used, user which runs library's functions has basically all access to it's code.

For example, they can overload libraries methods, like so:

Librarytestremove.UrlFetchApp = {
  fetch: function(url){
    console.log('check this out', url);
  }
};

var response = Librarytestremove.fetchData('10');

Result:

So, if you really have something to keep in secret - do not use Apps Script libraries.

If you have to use them - keep it minimal and reasonable.

Kos
  • 4,890
  • 9
  • 38
  • 42
  • Nice. But I don't think it'll work in case of installable edit trigger **with a fixed version**. Any changes made by editors will not be present in the saved version, at which installable trigger is set to run. – TheMaster Dec 28 '21 at 14:20
  • Well, if assume that editor of sheet can edit bound script, this can be done, but again, you share your file with editors and ask if they can be able to access part of a shared file, this is nonsense, this is lunacy from some point of view – Kos Dec 28 '21 at 16:12
  • They can edit bound script, but how can this be done? They or even the owner can't change a saved version, AFAIK. – TheMaster Dec 28 '21 at 17:05
  • 1
    @Kos, with the library function "fetchData(idFromSheetScript)" described in the original post, can you give an example how an editor of the sheet could log the "secret" variable from within the library? – que Dec 28 '21 at 19:09
  • @que do you have any questions about example already provided? Or you need certain amount of examples to be sure? – Kos Dec 28 '21 at 19:56
  • 1
    @Kos as far as I can tell, the examples already provided result in an error when used by an editor without access to the library project. The error is: "You do not have access to library , used by your script, or it has been deleted." – que Dec 28 '21 at 20:22