Skip to content

Commit

Permalink
feat: add --[no-]shell-escape flag [wip]
Browse files Browse the repository at this point in the history
This allows or prohibits shell command execution from within
TeX files.

TODO: Verify whether `latexmk -showextraoptions` mentions direct support
for passing either flag. latexmk(1) does not, but has examples passing
it via the `-pdf{lua,xe,}latex="COMMAND"` options. This would complicate
the construction of the latexmk command.

Fixes: #152
  • Loading branch information
dmke committed Nov 26, 2024
1 parent 161c307 commit 905515a
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 35 deletions.
27 changes: 20 additions & 7 deletions cmd/texd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ var opts = service.Options{
}

var (
engine = tex.DefaultEngine.Name()
jobdir = ""
pull = false
logLevel = zapcore.InfoLevel.String()
maxJobSize = units.BytesSize(float64(opts.MaxJobSize))
storageDSN = ""
showVersion = false
engine = tex.DefaultEngine.Name()
shellEscape = false
noShellEscape = false
jobdir = ""
pull = false
logLevel = zapcore.InfoLevel.String()
maxJobSize = units.BytesSize(float64(opts.MaxJobSize))
storageDSN = ""
showVersion = false

keepJobValues = map[int][]string{
service.KeepJobsNever: {"never"},
Expand Down Expand Up @@ -105,6 +107,10 @@ func parseFlags(progname string, args ...string) []string {
"bind `address` for the HTTP API")
fs.StringVarP(&engine, "tex-engine", "X", engine,
fmt.Sprintf("`name` of default TeX engine, acceptable values are: %v", tex.SupportedEngines()))
fs.BoolVarP(&shellEscape, "shell-escape", "", shellEscape,
"enable shell escaping to arbitrary commands (mutually exclusive with --no-shell-escape)")
fs.BoolVarP(&noShellEscape, "no-shell-escape", "", noShellEscape,
"enable shell escaping to arbitrary commands (mutually exclusive with --shell-escape)")

Check warning on line 113 in cmd/texd/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/texd/main.go#L110-L113

Added lines #L110 - L113 were not covered by tests
fs.DurationVarP(&opts.CompileTimeout, "compile-timeout", "t", opts.CompileTimeout,
"maximum rendering time")
fs.IntVarP(&opts.QueueLength, "parallel-jobs", "P", opts.QueueLength,
Expand Down Expand Up @@ -166,6 +172,13 @@ func main() { //nolint:funlen
zap.String("flag", "--tex-engine"),
zap.Error(err))
}
if shellEscape && noShellEscape {
log.Fatal("flags --shell-escape and --no-shell-escape are mutually exclusive")
} else if shellEscape {
_ = tex.SetShellEscaping(tex.AllowedShellEscape)
} else if noShellEscape {
_ = tex.SetShellEscaping(tex.ForbiddenShellEscape)
}

Check warning on line 181 in cmd/texd/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/texd/main.go#L175-L181

Added lines #L175 - L181 were not covered by tests
if maxsz, err := units.FromHumanSize(maxJobSize); err != nil {
log.Fatal("error parsing maximum job size",
zap.String("flag", "--max-job-size"),
Expand Down
65 changes: 58 additions & 7 deletions tex/engine.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
package tex

import "fmt"
import (
"fmt"
)

type Engine struct {
name string
flags []string
}

func NewEngine(name string, latexmkFlags ...string) Engine {
return Engine{name, latexmkFlags}
return Engine{name: name, flags: latexmkFlags}
}

func (e Engine) Name() string { return e.name }
func (e Engine) String() string { return e.name }
func (e Engine) Flags() []string { return e.flags }
func (e Engine) Name() string { return e.name }
func (e Engine) String() string { return e.name }

Check warning on line 17 in tex/engine.go

View check run for this annotation

Codecov / codecov/patch

tex/engine.go#L16-L17

Added lines #L16 - L17 were not covered by tests
func (e Engine) Flags() []string {
switch shellEscaping {
case RestrictedShellEscape:
return e.flags
case AllowedShellEscape:
return append([]string{"-shell-escape"}, e.flags...)
case ForbiddenShellEscape:
return append([]string{"-no-shell-escape"}, e.flags...)
}
panic("not reached")

Check warning on line 27 in tex/engine.go

View check run for this annotation

Codecov / codecov/patch

tex/engine.go#L27

Added line #L27 was not covered by tests
}

var (
engines = []Engine{
Expand Down Expand Up @@ -62,9 +74,9 @@ var LatexmkDefaultFlags = []string{
}

// LatexmkCmd builds a command line for latexmk invocation.
func (engine Engine) LatexmkCmd(main string) []string {
func (e Engine) LatexmkCmd(main string) []string {
lenDefaults := len(LatexmkDefaultFlags)
flags := engine.Flags()
flags := e.Flags()
lenFlags := len(flags)

cmd := make([]string, 1+lenDefaults+lenFlags+1)
Expand All @@ -75,3 +87,42 @@ func (engine Engine) LatexmkCmd(main string) []string {

return cmd
}

type ShellEscape int

const (
RestrictedShellEscape ShellEscape = iota // allows restricted command execution (e.g. bibtex)
AllowedShellEscape // allow arbitraty command execution
ForbiddenShellEscape // prohibit execution of any commands
maxShellEscape // must be last
)

type ErrUnexpectedShellEscape ShellEscape

func (err ErrUnexpectedShellEscape) Error() string {
return fmt.Sprintf("unexpected shell escaping value: %d", int(err))
}

var shellEscaping = RestrictedShellEscape

// SetShellEscaping globally configures which external programs the TeX compiler
// is allowd to execute. By default, only a restricted set of external programs
// are allowed, such as bibtex, kpsewhich, etc.
//
// When set to [ShellEscapeAllowed], the `-shell-escape` flag is passed to
// `latexmk`. Note that this enables arbitrary command execution, and consider
// the security implications.
//
// To disable any external command execution, use [ShellEscapeForbidden]. This
// is equivalent to passing `-no-shell-escape` to `latexmk`.

// Use [RestrictedShellEscape] to reset to the default value.
//
// Calling this with an unexpected value will return an error.
func SetShellEscaping(value ShellEscape) error {
if value < 0 || value >= maxShellEscape {
return ErrUnexpectedShellEscape(value)
}
shellEscaping = value
return nil
}
68 changes: 47 additions & 21 deletions tex/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,58 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEngine_LatexmkCmd(t *testing.T) {
t.Parallel()
t.Cleanup(func() { shellEscaping = 0 })

const mainInput = "test.tex"

for _, tc := range []struct {
flags []string
expected []string
}{
{
flags: nil,
expected: []string{"latexmk", "-cd", "-silent", "-pv-", "-pvc-", mainInput},
}, {
flags: []string{},
expected: []string{"latexmk", "-cd", "-silent", "-pv-", "-pvc-", mainInput},
}, {
flags: []string{"-single"},
expected: []string{"latexmk", "-cd", "-silent", "-pv-", "-pvc-", "-single", mainInput},
}, {
flags: []string{"-multiple", "-flags"},
expected: []string{"latexmk", "-cd", "-silent", "-pv-", "-pvc-", "-multiple", "-flags", mainInput},
},
} {
cmd := NewEngine("noname", tc.flags...).LatexmkCmd(mainInput)
assert.EqualValues(t, tc.expected, cmd)
for _, esc := range []ShellEscape{RestrictedShellEscape, AllowedShellEscape, ForbiddenShellEscape} {
require.NoError(t, SetShellEscaping(esc))

latexmk := []string{"latexmk", "-cd", "-silent", "-pv-", "-pvc-"}
shell := "restricted"
switch esc {
case RestrictedShellEscape:
// nothing to do
case AllowedShellEscape:
latexmk = append(latexmk, "-shell-escape")
shell = "allowed"
case ForbiddenShellEscape:
latexmk = append(latexmk, "-no-shell-escape")
shell = "forbidden"
}

for name, flags := range map[string][]string{
"nil": nil,
"empty": {},
"single": {"-single-flag"},
"multi": {"-multiple", "-flags"},
} {
t.Run(shell+"_"+name, func(t *testing.T) {
expected := make([]string, 0, len(latexmk)+len(flags)+1)
expected = append(expected, latexmk...)
expected = append(expected, flags...)
expected = append(expected, mainInput)

cmd := NewEngine("noname", flags...).LatexmkCmd(mainInput)
assert.EqualValues(t, expected, cmd)
})
}
}
}

func TestSetShellEscape(t *testing.T) {
require := require.New(t)
t.Cleanup(func() { shellEscaping = 0 })

require.NoError(SetShellEscaping(RestrictedShellEscape))
require.NoError(SetShellEscaping(AllowedShellEscape))
require.NoError(SetShellEscaping(ForbiddenShellEscape))

require.EqualError(SetShellEscaping(-1), "unexpected shell escaping value: -1")
require.EqualError(SetShellEscaping(maxShellEscape), "unexpected shell escaping value: 3")
require.EqualError(SetShellEscaping(maxShellEscape+1), "unexpected shell escaping value: 4")
}

0 comments on commit 905515a

Please sign in to comment.