<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: smithveg</title>
    <description>The latest articles on DEV Community by smithveg (@smithvegstack).</description>
    <link>https://dev.to/smithvegstack</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4112307%2F2388c0ca-2c9e-4924-ad46-67ae98e8bd39.png</url>
      <title>DEV Community: smithveg</title>
      <link>https://dev.to/smithvegstack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smithvegstack"/>
    <language>en</language>
    <item>
      <title>Sandboxing PDF Processing in PHP with Bubblewrap</title>
      <dc:creator>smithveg</dc:creator>
      <pubDate>Mon, 21 Sep 2026 18:13:34 +0000</pubDate>
      <link>https://dev.to/smithvegstack/sandboxing-pdf-processing-in-php-with-bubblewrap-5hj5</link>
      <guid>https://dev.to/smithvegstack/sandboxing-pdf-processing-in-php-with-bubblewrap-5hj5</guid>
      <description>&lt;p&gt;What happens when your PHP application processes an untrusted PDF with an external binary?&lt;/p&gt;

&lt;p&gt;Most of us never think about it. A user uploads a file, a controller stores it, a queued job runs a command-line tool on it, and the result goes back to the user.&lt;/p&gt;

&lt;p&gt;The tool might be a PDF engine, an image converter, an office-document converter, an OCR utility, or another document-processing binary. The code is often only a few lines long. It works, so it goes to production.&lt;/p&gt;

&lt;p&gt;This article is about the part that is easy to overlook: &lt;strong&gt;the security boundary around that one command&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It applies whether or not you ever use the library I describe at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The innocent-looking command
&lt;/h2&gt;

&lt;p&gt;Here is the shape of code that exists in a lot of applications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app/uploads/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$upload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stored_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'pdftotext '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' -'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or perhaps you use Symfony Process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Process\Process&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'/usr/local/bin/some-pdf-tool'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'input.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nv"&gt;$process&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version is already better from a command-injection perspective because the arguments are separated rather than concatenated into a shell command.&lt;/p&gt;

&lt;p&gt;But there is another question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What can that child process see if something goes wrong inside it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question matters because document parsers are complicated pieces of software processing files that an attacker may completely control.&lt;/p&gt;

&lt;p&gt;A PDF can contain compressed streams, fonts, images, object graphs, metadata, embedded files, malformed structures, unusual encodings and many other things.&lt;/p&gt;

&lt;p&gt;The parser has to deal with all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The hidden security boundary
&lt;/h2&gt;

&lt;p&gt;Imagine your Laravel queue worker runs as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;www-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it starts a PDF processor under the same Unix identity.&lt;/p&gt;

&lt;p&gt;Unless you deliberately restrict it, that child process may inherit access to whatever &lt;code&gt;www-data&lt;/code&gt; can access.&lt;/p&gt;

&lt;p&gt;Depending on the server, that may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application source code&lt;/li&gt;
&lt;li&gt;configuration files&lt;/li&gt;
&lt;li&gt;environment variables&lt;/li&gt;
&lt;li&gt;credentials available to the process&lt;/li&gt;
&lt;li&gt;other files owned by the same account&lt;/li&gt;
&lt;li&gt;other job directories&lt;/li&gt;
&lt;li&gt;network access&lt;/li&gt;
&lt;li&gt;writable temporary directories&lt;/li&gt;
&lt;li&gt;CPU, file descriptors and disk resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PDF processor probably does not need any of that.&lt;/p&gt;

&lt;p&gt;It may need only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/work/input.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and somewhere to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/work/output.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference became the design question for me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can the document-processing process see only the files required for one job, instead of everything the application account can reach?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the problem I wanted to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why input validation alone is not enough
&lt;/h2&gt;

&lt;p&gt;Input validation still matters.&lt;/p&gt;

&lt;p&gt;You should validate things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file type&lt;/li&gt;
&lt;li&gt;extension&lt;/li&gt;
&lt;li&gt;MIME type&lt;/li&gt;
&lt;li&gt;file size&lt;/li&gt;
&lt;li&gt;page count where appropriate&lt;/li&gt;
&lt;li&gt;command options&lt;/li&gt;
&lt;li&gt;filenames and paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But validation and sandboxing solve different problems.&lt;/p&gt;

&lt;p&gt;Validation asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should I accept this input?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sandboxing asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the processor is compromised anyway, what can it reach?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A perfectly valid PDF can still trigger a bug in a parser.&lt;/p&gt;

&lt;p&gt;Likewise, a newly discovered vulnerability may affect a file that passes every validation rule you currently have.&lt;/p&gt;

&lt;p&gt;So I treat the sandbox as &lt;strong&gt;defence-in-depth&lt;/strong&gt;, not as a substitute for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;patching&lt;/li&gt;
&lt;li&gt;least-privilege Unix accounts&lt;/li&gt;
&lt;li&gt;sensible server configuration&lt;/li&gt;
&lt;li&gt;process timeouts&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to prove that a document processor is safe.&lt;/p&gt;

&lt;p&gt;The goal is to &lt;strong&gt;reduce the attack surface and limit the blast radius&lt;/strong&gt; if it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Building a smaller execution boundary
&lt;/h2&gt;

&lt;p&gt;For Linux, I chose &lt;a href="https://github.com/containers/bubblewrap" rel="noopener noreferrer"&gt;Bubblewrap&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Bubblewrap is a small sandboxing tool that uses Linux namespaces and mount controls to construct a restricted environment for a process.&lt;/p&gt;

&lt;p&gt;The model I wanted was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PHP / Laravel application
        |
        | starts job
        v
Secure Runner
        |
        | creates Bubblewrap sandbox
        v
Document-processing binary
        |
        +-- sees /work
        +-- sees its executable
        +-- no inherited environment
        +-- no network by default
        +-- limited resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PHP application itself is &lt;strong&gt;not&lt;/strong&gt; inside the sandbox.&lt;/p&gt;

&lt;p&gt;Only the spawned document-processing process is.&lt;/p&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;

&lt;h3&gt;
  
  
  One workspace per job
&lt;/h3&gt;

&lt;p&gt;Each processing job gets a private directory.&lt;/p&gt;

&lt;p&gt;Inside the sandbox it is mounted as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The processor does not need the application's source tree.&lt;/p&gt;

&lt;p&gt;It does not need the parent jobs directory.&lt;/p&gt;

&lt;p&gt;It does not need neighbouring users' files.&lt;/p&gt;

&lt;p&gt;It only gets the workspace for that job.&lt;/p&gt;

&lt;h3&gt;
  
  
  No network by default
&lt;/h3&gt;

&lt;p&gt;The sandbox uses separate namespaces, and network access is disabled by default.&lt;/p&gt;

&lt;p&gt;For most PDF operations there is no legitimate reason for the binary processing a local file to connect to the Internet.&lt;/p&gt;

&lt;p&gt;If network access is actually required, it should be an explicit decision rather than an accidental inheritance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clear the environment
&lt;/h3&gt;

&lt;p&gt;A child process normally inherits environment variables from its parent.&lt;/p&gt;

&lt;p&gt;That may include information the document processor has no reason to know.&lt;/p&gt;

&lt;p&gt;The sandbox therefore starts with a cleared environment and only passes explicitly allowed variables.&lt;/p&gt;

&lt;p&gt;This changes the model from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;inherit everything except what we remember to remove&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;inherit nothing except what we deliberately add.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  No shell
&lt;/h3&gt;

&lt;p&gt;Executable and arguments are passed as an argv array through &lt;code&gt;proc_open()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;executable
argument 1
argument 2
argument 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"executable argument1 argument2 argument3"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no shell command string to interpret.&lt;/p&gt;

&lt;p&gt;This does not replace input validation, but it removes an unnecessary command-parsing layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time and resource limits
&lt;/h3&gt;

&lt;p&gt;A hostile document does not need remote code execution to cause trouble.&lt;/p&gt;

&lt;p&gt;It might simply make a parser consume resources.&lt;/p&gt;

&lt;p&gt;The runner therefore supports controls including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wall-clock timeout&lt;/li&gt;
&lt;li&gt;CPU limit through &lt;code&gt;prlimit&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;file-size limit&lt;/li&gt;
&lt;li&gt;open-file limit&lt;/li&gt;
&lt;li&gt;output-size cap&lt;/li&gt;
&lt;li&gt;disabled core dumps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the process exceeds its deadline, the sandbox and its descendants are terminated.&lt;/p&gt;

&lt;p&gt;This is useful for accidental runaway processing as well as deliberately hostile input.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fail closed
&lt;/h3&gt;

&lt;p&gt;One requirement mattered more than convenience:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If Bubblewrap is unavailable, do not quietly run the command without it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The runner throws an exception instead.&lt;/p&gt;

&lt;p&gt;That means a deployment problem becomes a failed processing job, rather than an invisible downgrade from sandboxed execution to normal execution.&lt;/p&gt;

&lt;p&gt;For security controls, I strongly prefer that behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The sandbox has limits
&lt;/h2&gt;

&lt;p&gt;This part is just as important as the feature list.&lt;/p&gt;

&lt;p&gt;The sandbox does &lt;strong&gt;not&lt;/strong&gt; automatically protect the entire PHP application.&lt;/p&gt;

&lt;p&gt;It does not sandbox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the parent PHP process&lt;/li&gt;
&lt;li&gt;the Laravel queue worker&lt;/li&gt;
&lt;li&gt;unrelated PHP code&lt;/li&gt;
&lt;li&gt;PHP work performed in-process before or after the runner&lt;/li&gt;
&lt;li&gt;the host kernel&lt;/li&gt;
&lt;li&gt;Bubblewrap itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if you decode a hostile image using an in-process PHP extension before invoking the sandbox, that decoding step is outside this boundary.&lt;/p&gt;

&lt;p&gt;The sandbox also cannot protect you from vulnerabilities in the Linux kernel or Bubblewrap that defeat the isolation mechanism itself.&lt;/p&gt;

&lt;p&gt;And the output file should still be treated as untrusted.&lt;/p&gt;

&lt;p&gt;If a tool creates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;output.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that does not magically mean the file is safe for every other parser that may later open it.&lt;/p&gt;

&lt;p&gt;Another current limitation is memory control.&lt;/p&gt;

&lt;p&gt;Without using cgroups or another external mechanism, memory is not capped by default.&lt;/p&gt;

&lt;p&gt;There is also no seccomp syscall filter applied by default.&lt;/p&gt;

&lt;p&gt;This is why I describe the project as a tool that &lt;strong&gt;reduces the attack surface&lt;/strong&gt;, not a universal security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. A PHP example
&lt;/h2&gt;

&lt;p&gt;The reusable part of this work became &lt;strong&gt;PDF-X Secure Runner&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is installable through Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require pdf-x/secure-runner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A basic example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PdfX\SecureRunner\&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;JobWorkspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;ResourceLimits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;SandboxConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;SecureRunner&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nv"&gt;$workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JobWorkspace&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/lib/myapp/jobs'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$workspace&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'in.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$uploadedBytes&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SecureRunner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;SandboxConfig&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$runner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'/usr/local/bin/pdfcpu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'info'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$workspace&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sandboxPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'in.pdf'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;limits&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResourceLimits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isSuccessful&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$workspace&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application writes the input into the job workspace.&lt;/p&gt;

&lt;p&gt;Inside the sandbox, the processor sees that workspace as &lt;code&gt;/work&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$workspace&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sandboxPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'in.pdf'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;provides the path from the sandbox's point of view.&lt;/p&gt;

&lt;p&gt;The binary does not need access to the real host path.&lt;/p&gt;

&lt;p&gt;For produced files, the application can resolve outputs back through the workspace rather than blindly trusting arbitrary paths returned by the child process.&lt;/p&gt;

&lt;p&gt;The workspace logic also rejects symlinks and paths escaping the job directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static and dynamic binaries
&lt;/h3&gt;

&lt;p&gt;A statically linked binary may need almost nothing except its executable and the job workspace.&lt;/p&gt;

&lt;p&gt;A dynamically linked binary needs its runtime libraries.&lt;/p&gt;

&lt;p&gt;For that case the configuration can explicitly expose the required system libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SandboxConfig&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withSystemLibraries&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a security trade-off here.&lt;/p&gt;

&lt;p&gt;Every additional read-only path you expose becomes visible to the sandboxed process.&lt;/p&gt;

&lt;p&gt;So the default should remain as narrow as practical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Passwords and secrets
&lt;/h3&gt;

&lt;p&gt;Another small but important point: avoid putting secrets in command arguments where possible.&lt;/p&gt;

&lt;p&gt;Arguments may be visible in process listings on the host.&lt;/p&gt;

&lt;p&gt;If a tool accepts a password or secret over standard input, prefer that mechanism.&lt;/p&gt;

&lt;p&gt;The runner supports passing stdin separately from the argv array.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Testing the boundary
&lt;/h2&gt;

&lt;p&gt;Sandbox code is easy to write incorrectly.&lt;/p&gt;

&lt;p&gt;So I wanted more than unit tests around PHP objects.&lt;/p&gt;

&lt;p&gt;The package includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vendor/bin/pdfx-sandbox-check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The self-check uses generated canary files to test properties such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bubblewrap availability&lt;/li&gt;
&lt;li&gt;workspace visibility&lt;/li&gt;
&lt;li&gt;parent-directory isolation&lt;/li&gt;
&lt;li&gt;sibling-job isolation&lt;/li&gt;
&lt;li&gt;environment leakage&lt;/li&gt;
&lt;li&gt;network access&lt;/li&gt;
&lt;li&gt;writes outside the workspace&lt;/li&gt;
&lt;li&gt;symlink escape behaviour&lt;/li&gt;
&lt;li&gt;timeout and process cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It reports PASS or FAIL.&lt;/p&gt;

&lt;p&gt;It does not inspect real secrets.&lt;/p&gt;

&lt;p&gt;It does not try to prove the system is universally secure.&lt;/p&gt;

&lt;p&gt;Its job is narrower:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this host appear to enforce the containment behaviour the application expects?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That matters because Linux distributions and hosting environments can differ in their user-namespace policies.&lt;/p&gt;

&lt;p&gt;A configuration that works on one server may fail on another.&lt;/p&gt;

&lt;p&gt;For that reason, the self-test should be run on every host where the package is deployed.&lt;/p&gt;

&lt;p&gt;The project also runs its Linux containment suite in CI with Bubblewrap installed.&lt;/p&gt;

&lt;p&gt;On platforms without Bubblewrap, such as my macOS development machine, the sandbox itself cannot run. The library is designed to fail closed in that situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Why I open-sourced it
&lt;/h2&gt;

&lt;p&gt;This started while I was building &lt;a href="https://pdf-x.co" rel="noopener noreferrer"&gt;PDF-X&lt;/a&gt;, an online PDF-processing service.&lt;/p&gt;

&lt;p&gt;The original question was specific:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How should I run document-processing binaries against files uploaded by strangers?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the answer was not really specific to PDF-X.&lt;/p&gt;

&lt;p&gt;Any PHP application may eventually need to execute an external tool against un&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>laravel</category>
      <category>linux</category>
    </item>
    <item>
      <title>SmoothDelete: Animated AJAX Row Deletion for Modern and Legacy Web Apps</title>
      <dc:creator>smithveg</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:48:47 +0000</pubDate>
      <link>https://dev.to/smithvegstack/smoothdelete-animated-ajax-row-deletion-for-modern-and-legacy-web-apps-3mbl</link>
      <guid>https://dev.to/smithvegstack/smoothdelete-animated-ajax-row-deletion-for-modern-and-legacy-web-apps-3mbl</guid>
      <description>&lt;p&gt;Deleting one table row should feel immediate and polished, even in older business systems. Most CRUD scaffolding deletes a row by navigating to a URL and reloading the whole page — which resets your scroll position, filters, and pagination, and makes even a solid backend feel outdated.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;SmoothDelete&lt;/strong&gt;: a small, reusable pattern for deleting a record asynchronously and animating the row out of the interface, with no page reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A dedicated AJAX endpoint handles the delete and returns JSON (&lt;code&gt;{"success": true}&lt;/code&gt; or &lt;code&gt;{"success": false, "message": "..."}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;On success, the row gets a CSS transition, then — in the next animation frame — fades and scales down slightly (the one-frame delay matters: set both the start and end state in the same tick and the browser won't animate anything)&lt;/li&gt;
&lt;li&gt;After the fade, the row's height collapses (padding + &lt;code&gt;scaleY(0)&lt;/code&gt;) so it doesn't leave a blank gap&lt;/li&gt;
&lt;li&gt;The DOM node is removed, and if the table's now empty, an empty-state message shows&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's framework-free at its core (&lt;code&gt;assets/smooth-delete.js&lt;/code&gt; + &lt;code&gt;.css&lt;/code&gt;), with a legacy jQuery version for older codebases, and a full working PHP + PDO/SQLite reference backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe mode vs. optimistic mode
&lt;/h2&gt;

&lt;p&gt;One thing I wanted to get right: two documented deletion modes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Safe mode (default):&lt;/strong&gt; the row disappears only after the server confirms the delete succeeded. On failure, the button re-enables and shows an error. UI and database never disagree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic mode:&lt;/strong&gt; the row disappears the instant the request settles — even on failure — for cases where instant feedback matters more, with a visible warning if the delete wasn't actually confirmed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hiding a row client-side is never proof the database row is gone — every backend example validates, authorizes, and checks CSRF before deleting anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
cd examples/plain-php
php -S localhost:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>javascript</category>
      <category>php</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
