top of page

Security Best Practices

It's easy to think security doesn't apply to your extension. After all, it's just a SketchUp plugin, who would bother? But extensions handle files, user input, and sometimes talk to servers. A little carelessness can open the door to things you didn't intend.

This article covers common security pitfalls in SketchUp extension development and how to avoid them. Most of the fixes are small, often just using a different method, and each one makes your extension a little more robust. Individual issues may seem harmless in isolation, but like the Swiss cheese model used in aviation safety, problems occur when the holes align. Follow good practices consistently to ensure they never do.

Code Injection

Code injection occurs when user-supplied data is interpreted as code rather than as data. This can happen in several contexts within SketchUp extensions.

JavaScript Injection

When communicating between Ruby and JavaScript via HtmlDialog#execute_script, you may interpolate Ruby variables directly into JavaScript strings. This is fragile and dangerous.

# Bad - breaks if message contains quotes, vulnerable to injection
message = "Hello"
dialog.execute_script("showMessage('#{message}')")

If message comes from user input, a component name, or model attributes, it could contain a single quote and break the JavaScript. Worse, a malicious value could execute arbitrary JavaScript code.

message = "6'"
# Becomes: showMessage('6'')

message = "'); alert('Code injection!"
# Becomes: showMessage(''); alert('Code injection!')

The fix is to use Ruby's JSON library. The to_json method properly escapes strings and adds the surrounding quotes.

# Good - properly escaped, handles any string content
require "json"
message = "Hello"
dialog.execute_script("showMessage(#{message.to_json})")

Note that to_json adds the quotes around the string. Don't add extra quotes yourself or you'll undo the protection.

# Bad - extra quotes break the escaping
dialog.execute_script("showMessage('#{message.to_json}')")

Also note that manual escape attempts with `gsub` are often error prone and unsafe.

# Bad - fails on backslash
message = "\\'); alert(1)//"
dialog.execute_script("showMessage('#{message.gsub("'", "\\\\'")}')")

This pattern applies to any data type. Numbers, arrays, and hashes can all be safely passed using to_json.

data = { name: "Component <1>"width: 100 }
dialog.execute_script("processData(#{data.to_json})")

HTML Injection

When generating HTML content dynamically, user data must be escaped to prevent it from being interpreted as HTML markup. This doesn't just apply to intentional attacks, even innocent data can break things. A component named "I <3 SketchUp" would be treated as an HTML tag starting at <3.

The safest approach is to use a modern JavaScript framework like Vue or React. These frameworks automatically escape data when rendering, preventing injection by default.

// Vue - automatically escapes the name
const name = "<script>alert('Code injection!')</script>";

<template>
  <div>{{ name }}</div>
</template>

When not using a framework, prefer textContent= over innerHTML=. The textContent property treats the value as plain text, not HTML.

// Bad - interprets value as HTML
element.innerHTML = name;

// Good - treats value as plain text
element.textContent = name;

Be careful with HTML attributes too. Attributes like src and href, can contain a javascript: URI, which executes code when activated.

<!-- Bad - user-controlled URL could be javascript: -->
<img src="javascript:alert('Code injection!')">
<a href="javascript:alert('Code injection!')">Click me</a>

When setting URLs from user data, whitelist safe schemes rather than trying to block dangerous ones — there may be dangerous schemes you haven't thought of.

const SAFE_SCHEMES = ["https:""file:"];

function isSafeURL(url) {
  try {
    const parsed = new URL(url, window.location.href);
    return SAFE_SCHEMES.includes(parsed.protocol);
  } catch {
    return false;
  }
}

if (isSafeURL(userURL)) {
  link.href = userURL;
}

If you are not using a framework, prefer interacting with the HTML Document Object Model programmatically, not by generating a HTML string. The DOM API handles escaping automatically with textContent and setAttribute. The DOM approach also allows creating new elements and editing existing ones with similar code.

// Bad 
html = '<div><span>' + variable + '</span></div>'

// Good
var span = document.createElement('span');
span.textContent = variable;
var div = document.createElement('div');
div.appendChild(span);

If you must generate HTML strings in Javascript, e.g. in a legacy code base you don't want to completely rewrite, make sure to escape the HTML control characters whenever values are inserted to HTML.

// Okay
function escapeHTML(text) {
  return text
    .replace(/&/g"&amp;")
    .replace(/</g"&lt;")
    .replace(/>/g"&gt;")
    .replace(/"/g"&quot;")
    .replace(/'/g"&#39;");
}
variable = "I <3 SketchUp";
html = '<div><span>' + escapeHTML(variable) + '</span></div>';

If you must generate HTML strings in Ruby before sending them to the dialog, use CGI.escape_html to convert special characters to HTML entities.

# Bad - user data interpreted as HTML
name = "<script>alert('Code injection!')</script>"
html = "<div>#{name}</div>"

# Good - special characters are escaped
require "cgi"
name = "<script>alert('Code injection!')</script>"
html = "<div>#{CGI.escape_html(name)}</div>"
# Result: <div>&lt;script&gt;alert('Code injection!')&lt;/script&gt;</div>

Escaping is especially important when displaying component names, tag names, or any other data that might come from a model file you didn't create.

Ruby Code Injection

The eval method executes arbitrary Ruby code from a string. This is inherently dangerous and is not allowed in SketchUp extensions.

# Bad - never do this
eval(code)

Even with sanitization attempts, eval is difficult to make safe. Ruby is a dynamic language with many ways to access dangerous functionality indirectly. A determined attacker can bypass string filters using encoding tricks, send, const_get, or any number of dynamic features. The only safe approach is to not use eval at all.

The string forms of instance_eval and class_eval carry the same risks — they execute arbitrary code from a string.

# Bad - same danger as eval
object.instance_eval(code_string)
klass.class_eval(code_string)

If you need these for metaprogramming, use the block form instead. Blocks are compiled at load time and cannot be injected into at runtime.

# Good - block form, no string interpretation
object.instance_eval { @name }
klass.class_eval { define_method(:greet) { "hello" } }

See Alternatives to Eval for safe replacements for common use cases like mathematical expressions, dynamic method calls, and configuration files.

Unsafe Deserialization

Marshal.load is effectively eval in disguise. When Ruby deserializes a Marshal blob, it can instantiate arbitrary objects and trigger methods as part of the process. If the data comes from a file, model attribute, or network response, an attacker can craft a payload that executes code on load.

# Bad - as dangerous as eval
blob = Sketchup.active_model.get_attribute("my_ext""data")
data = Marshal.load(blob)

Use a safe data format like JSON instead. JSON can only represent simple values — strings, numbers, arrays, and hashes — so it cannot trigger code execution during parsing.

# Good - JSON cannot execute code
require "json"
json_string = Sketchup.active_model.get_attribute("my_ext""data")
data = JSON.parse(json_string)

Command Injection

If you must use system commands, never interpolate user data directly into the command string.

# Bad - vulnerable to command injection
filename = user_input
system("process_file '#{filename}'")

A malicious filename like '; rm -rf / # would execute destructive commands.

The same applies to backticks and %x{}.

# Bad
output = `process_file '#{filename}'`
output = %x(process_file '#{filename}')

When you need to capture output, use Open3 from the standard library.

# Good - arguments passed safely, output captured
require "open3"
output, status = Open3.capture2("process_file"filename)

For cases where you don't need the output, pass arguments as separate array elements to system.

# Good - arguments passed safely
filename = user_input
system("process_file", filename)

Better yet, prefer Ruby's standard library or the SketchUp API over system commands. There is often a better way than to shell out to an external program.

# Bad - shelling out to gzip
system("gzip""-d"gz_path)

# Good - using Ruby's standard library
require "zlib"

Zlib::GzipReader.open(gz_pathdo |gz|
  File.binwrite(output_path, gz.read)
end

Path Traversal & Unexpected File Types

When downloading files or accepting file paths from external sources, validate that the path doesn't escape its intended directory. Also validate file extensions to prevent downloading unexpected file types.

# Bad - allows escaping the target directory, could be naughty file
filename = downloaded_filename # Could be "../../../etc/passwd"
path = File.join(download_dir, filename)
File.write(path, content)

Verify that the final path is within the intended directory and with an expected file type.

# Good - validate the resolved path
filename = downloaded_filename
# Expand both paths so start_with? compares the same canonical form.
download_dir_expanded = File.expand_path(download_dir)
path = File.expand_path(File.join(download_dir_expanded, filename))

unless path.start_with?(download_dir_expanded + "/")
  raise "Path traversal detected"
end

extension = File.extname(filename).downcase
unless [".png"".jpg"".jpeg"".skp"].include?(extension)
  raise "Invalid file extension"
end

File.write(path, content)

Sensitive Data

Be careful with data that could be sensitive.

Never log passwords, API keys, or authentication tokens, even in debug mode.

# Bad
puts "Authenticating with token: #{api_token}" if DEBUG

# Good
puts "Authenticating..." if DEBUG # Don't include the token

Network Security

When making HTTP requests, prefer HTTPS over HTTP to prevent man-in-the-middle attacks.

# Bad
uri = "http://api.example.com/data"

# Good
uri = "https://api.example.com/data"

When your extension communicates with a server, validate that responses are well-formed before processing them. Don't assume the server will always return valid data.

Do not execute code fetched from a server (other than the Javascript running inside of the HtmlDialog).

WebDialog

WebDialog was deprecated over a decade ago and has known security vulnerabilities. Use HtmlDialog instead.

# Bad
dialog = UI::WebDialog.new("My Dialog")

# Good
dialog = UI::HtmlDialog.new(dialog_title: "My Dialog")

If you must support SketchUp versions older than 2017, use WebDialog as a fallback only.

# Acceptable
if defined?(UI::HtmlDialog)
  dialog = UI::HtmlDialog.new(dialog_title: "My Dialog")
else
  dialog = UI::WebDialog.new("My Dialog")
end

Third-Party Updates

SketchUp and Extension Warehouse provide infrastructure for notifying users of extension updates. Do not implement your own update mechanism that downloads and installs new code.

# Bad - bypasses security review
def check_for_updates
  new_code = download_from_server
  eval(new_code) # Extremely dangerous
end

Third-party update mechanisms bypass the Extension Warehouse review process, potentially introducing dangerous code. Instead, publish updates through Extension Warehouse and let SketchUp notify users.

Summary

The biggest security risk isn't a specific line of code — it's complacency. When an extension works fine for you, it's tempting to think "good enough" and move on. But your users might open model files from strangers, paste unexpected values into input fields, or run your extension on a slow connection where a server response comes back garbled. These are the moments where sloppy data handling turns into a real problem.

The good news is that most fixes are straightforward:

  1. Never trust external data. Escape or validate everything that comes from users, files, or servers.

  2. Let libraries handle encoding: to_json for JavaScript, CGI.escape_html for HTML.

  3. Do not use eval and be careful with system.

  4. Constrain file operations to the directories and types you expect.

  5. Use HtmlDialog instead of WebDialog.

  6. Don't implement your own update mechanisms.

None of these are hard to do. The hard part is remembering to do them every time, even when it feels unnecessary. Build the habit, and your users won't have to worry.

Further Reading

Alternatives to Eval

Rescue in SketchUp

Creating a SketchUp Extension

Other Resources

XSS Prevention Cheat Sheet

Extension Requirements

bottom of page