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

Add ability to simulate many ALD_Investment_ec_base definitions #1081

Merged
merged 15 commits into from
Dec 7, 2016
Merged

Add ability to simulate many ALD_Investment_ec_base definitions #1081

merged 15 commits into from
Dec 7, 2016

Conversation

martinholmer
Copy link
Collaborator

Recently an alternative definition of the investment income exclusion base was requested by a model user, and the boolean _ALD_Investment_ec_base_all parameter was added to handle this alternative. But this is not a very good solution. What happens when a third (or fourth or fifth) definition is requested? In that case, we would have to replace the boolean parameter _ALD_Investment_ec_base_all with _ALD_Investment_ec_base_type that has numerical values corresponding to each definition. The code would have a long if-elif statement and it would have to be changed each time a new definition was requested. Plus, users would have to remember what definition each _ALD_Investment_ec_base_type value referred to. Not a pretty picture.

This pull request handles this problem in a different way, using one possible implementation of the ideas discussed in issue #429. The basic idea is to create a policy parameter that is a Python expression rather than a number. In this pull request, the boolean _ALD_Investment_ec_base_all parameter is replaced by two parameters: (a) the ALD_Investment_ec_base_code parameter that is a string containing the Python expression that defines alternative definitions of the base, and (b) the boolean _ALD_Investment_ec_base_code_active parameter that specifies which years the expression is used to compute the base. In years the code expression is inactive, the base is computed the way it is now when _ALD_Investment_ec_base_all is true.

This pull request can be viewed as a trial-run of the general ideas discussed in #429. I'm sure there are other ways to implement those ideas, so this pull request should restart the #429 discussion.

Below I show how Tax-Calculator works on the branch contained in this pull request.

Comments are welcome after Thanksgiving.

@MattHJensen @feenberg @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher @codykallen

$ cd ~/work/OSPC/tax-calculator
$ cp ../tax-calculator-data/puf.csv .

$ git checkout master
$ cat ref-old.json
{
    "_ALD_Investment_ec_base_all": {"2016": [false]},
    "_ALD_Investment_ec_rt": {"2016": [0.50]}
}
$ python inctax.py puf.csv 2016 --reform ref-old.json 
$ ls -l puf-16.out-inctax-ref-old
-rw-r--r--  1 mrh  staff  37776134 Nov 23 14:30 puf-16.out-inctax-ref-old

$ git checkout invinc-ec-base-code
Switched to branch 'invinc-ec-base-code'
$ cat ref-new.json 
{
    "_ALD_Investment_ec_base_code_active": {"2016": [true]},
    "param_code": {"ALD_Investment_ec_base_code": "e00300 + e00650 + p23250"},
    "_ALD_Investment_ec_rt": {"2016": [0.50]}
}
$ python inctax.py puf.csv 2016 --reform ref-new.json 
$ ls -l puf-16.out-inctax-ref-new
-rw-r--r--  1 mrh  staff  37776134 Nov 23 14:33 puf-16.out-inctax-ref-new

$ diff puf-16.out-inctax-ref-new puf-16.out-inctax-ref-old
$ 

So, when we use the code parameter, we get exactly the same results as when using the old boolean parameter.

@codecov-io
Copy link

codecov-io commented Nov 23, 2016

Current coverage is 98.79% (diff: 100%)

Merging #1081 into master will increase coverage by 0.01%

@@             master      #1081   diff @@
==========================================
  Files            38         38          
  Lines          2795       2830    +35   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits           2761       2796    +35   
  Misses           34         34          
  Partials          0          0          

Powered by Codecov. Last update 371ed5e...d7fd3b6

@martinholmer
Copy link
Collaborator Author

Here is another example of a Python expression as a policy parameter using the code contained in pull request #1081. This useless example demonstrates how to use an expression to duplicate the default calculation of the investment income exclusion base. This example is interesting in that it shows how numpy array functions (maximum, minimum, and where) can be used in expressions.

$ date
Wed Nov 23 18:03:11 EST 2016
$ cd ~/work/OSPC/tax-calculator
$ git checkout invinc-ec-base-code
$ cp ../tax-calculator-data/puf.csv .
$ cat ref-new1.json
{
    "_ALD_Investment_ec_rt": {"2015": [0.50]}
}
$ cat ref-new2.json
{
    "param_code": {"ALD_Investment_ec_base_code":
      "e00300 + e00600 + max(-3000./_sep, p22250+p23250) + e01100 + e01200"},
    "_ALD_Investment_ec_base_code_active": {"2015": [true]},
    "_ALD_Investment_ec_rt": {"2015": [0.50]}
}
$ python inctax.py puf.csv 2015 --reform ref-new1.json
$ python inctax.py puf.csv 2015 --reform ref-new2.json
$ ls -l puf-15*
-rw-r--r--  1 mrh  staff  37748055 Nov 23 18:05 puf-15.out-inctax-ref-new1
-rw-r--r--  1 mrh  staff  37748055 Nov 23 18:06 puf-15.out-inctax-ref-new2
$ diff puf-15.out-inctax-ref-new1 puf-15.out-inctax-ref-new2
$ 

So, we get exactly the same results using either the (faster) built-in method (ref-new1.json) or the (slower) expression method (ref-new2.json) of calculating the exclusion base.

@MattHJensen @feenberg @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher @codykallen

@feenberg
Copy link
Contributor

feenberg commented Nov 23, 2016 via email

@martinholmer
Copy link
Collaborator Author

@feenberg commented (after receiving the original comment in the #1081 conversation):

That is great, important progress.

We have to worry about security though. Is there some way to know that the submitted expression can't have any side-effects? Is python susceptible to the old

x = y;<bad code>;

trick, where the bad code is hidden behind a semicolon which terminates the expression? I did figure out that Python compilers don't have a flag to emit only "pure code" with no side effects, so I think it is up to us.

I'm glad you are generally positive about the approach taken in pull request #1081.

Let me address your security concerns, which were expressed quite clearly in the discussion of issue #429.

The simple answer is that this approach is very secure and cannot have any undesirable side effects.

Let me explain the approach in more detail so that you can see why I answered your question the way I did. First, the param_code is not one or more statements (like in your example above), but rather a single expression. An expression in Python is anything that can be on the right-hand side of an assignment statement. Second, the Tax-Calculator variables that can be used in the expression can be limited to variables that are appropriate to the expression. And third, the functions that can be used in the expression can be limited to functions that are appropriate to the expression. So, in pull request #1081, the key code is as follows:

def ALD_Investment_ec_base_code_function(calc):
    """
    Compute investment_ec_base from code
    """
    code = calc.policy.param_code['ALD_Investment_ec_base_code']
    visible = {'min': np.minimum, 'max': np.maximum, 'where': np.where}
    vars = ['e00300', 'e00600', 'e00650', 'e01100', 'e01200',
            'p22250', 'p23250', '_sep']
    for var in vars:
        visible[var] = getattr(calc.records, var)
    # pylint: disable=eval-used
    calc.records.investment_ec_base = eval(compile(code, '<str>', 'eval'),
                                           {'__builtins__': None}, visible)

So, in this case only the numpy minimum, maximum, and where (if-like) functions are available in the expression and only variables that are components of investment income are available to the expression writer. The Python compile function will generate an error if there are statements (rather than just one right-hand-side expression), and the Python eval function will generate an error if any function or variable that is not in the visible directory is used. So, things seem pretty secure to me. Can you see any security holes in this approach?

@MattHJensen @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher @codykallen

@feenberg
Copy link
Contributor

feenberg commented Nov 29, 2016 via email

@martinholmer
Copy link
Collaborator Author

@feenberg said:

I was thinking of
https://xkcd.com/327/
but I see the "eval" function is the key. That seems very effective.

Agreed. Python has an exec function that allows any number of statements, but that seemed like more of a security problem than it was worth.

@MattHJensen @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher @codykallen

@zrisher
Copy link
Contributor

zrisher commented Dec 1, 2016

@martinholmer I like where you're going with this. I don't think it's safe for direct use by web users by any means (not sure if that's what you're asking), but it's totally safe for local use. It's making me think about ways we could package general changes to tax calculation logic that would support exposing control in a safe manner online, making use of a user review component as @feenberg smartly suggested. I'm going to mull it over and will get back with more commentary later today.

@martinholmer
Copy link
Collaborator Author

@zrisher said:

I like where you're going with this. I don't think it's safe for direct use by web users by any means (not sure if that's what you're asking), but it's totally safe for local use. It's making me think about ways we could package general changes to tax calculation logic that would support exposing control in a safe manner online, making use of a user review component as @feenberg smartly suggested. I'm going to mull it over and will get back with more commentary later today.

Why do you say "I don't think it's [#1081] safe for direct use by web users by any means ..."?

@MattHJensen @feenberg

@feenberg
Copy link
Contributor

feenberg commented Dec 1, 2016 via email

@zrisher
Copy link
Contributor

zrisher commented Dec 1, 2016

@martinholmer said:

Why do you say "I don't think it's [#1081] safe for direct use by web users by any means ..."?

The links I listed when discussing sandboxing in #429 lay this out, but it's a bit of reading and I should have summarized:

  • A user could cause any exception they want - Ok so we catch exceptions and don't return them explicitly via the web
  • A user could lock up the CPU or overflow the stack - Ok so we put some controls around its execution. But you quickly find holes, and you keep further removing the control from execution until you arrive at the managed-thread architecture provided by PyPy's sandbox.
  • A user could control things we don't want them to - This is inherently difficult to solve because Python provides such powerful metaprogramming abilities regardless of your execution level. This is exemplified by the links @feenberg provided, explained by the top answer to this SO post, and proven out by the death of the language-level sandbox pysandbox. PyPy attempts to solve this by replacing even the lowest level references with its safer constructs.
  • Also, in our particular case, a user could modify tax logic to try to access the underlying taxpayer data. I don't know how dropq works, so I'm not sure if it relies on a bit a of indirection existing in the dependent system or not.

HOWEVER, if you structure your code submission process in such a way to make visual review of top additions by project maintainers feasible, web-based code modification could become a reality for this project. I would still recommend running the calculations within a PyPy sandbox, or else your entire system is only as safe as the user accounts that can approve submissions. And I'm not sure about the data exposure issues, but I'm hoping dropq already solves them.

@feenberg
Copy link
Contributor

feenberg commented Dec 1, 2016 via email

@zrisher
Copy link
Contributor

zrisher commented Dec 1, 2016

@feenberg said:

Don't the simulations run on a separate computer? If that computer
crashes, does it affect any other visitor? Do we have a way to kill a job
in an infinite loop before too much money has been spent?

I'm pretty sure there are separate servers for webapp-public vs the taxcalc workers they queue up, but if an anonymous user has the ability to run unsandboxed code without review, they can bring down the entire tax calculation side by simply locking up all the workers with multiple long-running requests. Even with auto-scaling, it's an easy way to deny that service. I'm sure the worker controller has some safeguards built in here that @talumbau could speak to, but I'd bet that no matter how it's architected, a malicious user could easily deny the tax calculation service in that situation.

But in terms of service ability and data security, the greater danger is actually this:

A user could control things we don't want them to

With the ability to run arbitrary code, a user might be able to gain access to the underlying OS, and possibly even the entire AWS account.

Simply put, without at least the PyPy sandbox or code review, I would not recommend allowing the execution of user-supplied code if you care about the availability or security of your AWS assets. And in the long term I highly suggest both. @talumbau is probably best suited to speak to the current architecture and its security, though.

@martinholmer
Copy link
Collaborator Author

Commit e545fa9 in pull request #1081 adds some security protection by scanning the parameter code for dangerous elements. The first version of the new Policy.scan_param_code function raises a ValueError if the parameter code contains one of the following character strings: "__" (that's a double underscore), "lambda", or "[". These seem to rule out code that causes problems in the various links that @feenberg and @zrisher provided earlier in the #1081 conversation.

Are there other dangerous character strings that should be prohibited?

How far does this go in making secure our particular use of eval(compile(code, ...), ...)?
In other words, can you provide examples of Python expressions that would get through this scan and cause some kind of problem?

@MattHJensen @feenberg @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher @codykallen

@zrisher
Copy link
Contributor

zrisher commented Dec 2, 2016

@martinholmer

Both of these lock up my process for a long time and could be amended to overflow memory instead:

  • 9999**99999999 - Basic mathematical operations with large numbers
  • ''.join(str(x / (y + 1)) for x in xrange(999999) for y in xrange(999999)) - List comprehension without the use of brackets

The approach you're taking is language-level sandboxing, the same path that the creator of pysandbox tried and declared fundamentally flawed. The current consensus seems to be that only OS-level sandboxes are effective for Python.

I am curious if our incredibly simplified use case for tax calculation logic with a few variables and basic operations might be an exception to that rule, but I am doubtful. Even if we massage it to the point that our team can't prove it's not safe, we don't have the security background of pysandbox's maintainers, nor its hundreds of experienced black-hat engineers openly testing it for years.

@zrisher
Copy link
Contributor

zrisher commented Dec 2, 2016

It would be amazing if we had a working version of that though, and I feel like I'm just repeating myself, so I'm going to lay off this issue a bit and see if other people can make more useful contributions.

@feenberg
Copy link
Contributor

feenberg commented Dec 2, 2016 via email

@martinholmer
Copy link
Collaborator Author

@zrisher and @feenberg, Thank you both for the citations in the security literature and for your thoughtful comments on the pros/cons of using in pull request #1081 the following type of code:

eval(compile(code, '<str>', 'eval'), 
     {'__builtins__': {}},
     dictionary_of_visible_functions_and_variables)

There is a lot in the citations and in what you say, so I'm going to make a number of #1081 comments to discuss the issues raised by your comments. Thanks again for all the help.

@MattHJensen @Amy-Xu @GoFroggyRun @andersonfrailey @codykallen

@martinholmer
Copy link
Collaborator Author

@zrisher said (among a number of insightful things):

I don't think it [i.e., pull request #1081] is safe for direct use by web users by any means [...], but it's totally safe for local use.

Since making that statement, #1081 has added a Policy.scan_param_code function that rejects parameter code that includes any of the following four character strings: __ (that's a double underscore), lambda, [, and **. And the eval function is called this way:

eval(compile(code, '<str>', 'eval'), 
     {'__builtins__': {}},
     dictionary_of_visible_functions_and_variables)

The combination of code scanning and the no builtins means that all the provided examples of malicious code that gained access to the operating system running the Python interpreter have been avoided. Also, the kinds of CPU-wasting expressions that @zrisher pointed out have been avoided (see more detail on my experiments at the end of this comment).

Despite these efforts, it seems likely (as @zrisher imagines) that a skilled hacker could still figure out some kind of security vulnerability with this arrangement. So, the question about whether or not to deploy the enhancements in #1081 in TaxBrain is (as @feenberg points out) really a benefit-cost analysis. There is no incentive to engage in this sort of hacking when running Tax-Calculator on your local computer because the hacker would pay the price. As @zrisher points out, the issue is whether or not to deploy #1081 on TaxBrain.

Let me point out that it would be simple to delay implementing #1081 on TaxBrain by simply having TaxBrain reject the upload of any JSON reform files that contains the phrase param_code.

What would the situation be like if that were not done and TaxBrain did accept for execution JSON reform files that contain param_code? It does seem like there would be some security risks, but as @feenberg points out hacker attacks are not likely to be that frequent. On the other hand, the flexibility of specifying a broader range of reform provisions with code (instead of numerical parameters) would be a clear benefit. And that benefit would grow as other parameter code options were added into Tax-Calculator. Note that the addition of more code parameters does not increase security risks because all a hacker needs is one code parameter. Code parameters are an advanced feature, so I don't think there is much reason to add "code boxes" on the standard TaxBrain input page because new code parameters can always be handled via the JSON reform file upload capability without any changes in the TaxBrain interface.

@MattHJensen @Amy-Xu @GoFroggyRun @andersonfrailey @codykallen

POSTSCRIPT ON TRYING MALICIOUS CODE
Different versions of the the following JSON reform file were executed under the #1081 code as follows:

$ python inctax.py puf.csv 2016 --reform bad-code.json

Here is the content of the bad-code.json file:

{
    "_ALD_Investment_ec_base_code_active": {"2016": [true]},
    "param_code": {"ALD_Investment_ec_base_code":
                   "e00300 + e00650 + p23250"}

//(0) "e00300 + e00650 + p23250"
// Code (0) is an example of legitimate code.

//(1) "[c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__=='Quitter'][0](0)()"
// Code (1) causes Python interpreter to quit, but is now illegal given
// Policy.scan_param_code() logic.

//(2) "(lambda fc=(lambda n: [c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == n][0]): fc('function')(fc('code')(0,0,0,0,'KABOOM',(), (),(),'','',0,''),{})())()"
// Code (2) causes segmentation fault 11, but is now illegal given
// Policy.scan_param_code() logic.

//(3) "9999**99999999"
// Code (3) causes eval(compile(code,...),...) step to block, but is now
// illegal given Policy.scan_param_code() logic.
// *** Notice that this expression has to have an integer base and an integer
//     power to cause the compile(code,...) function to block.
// *** Other large number expressions raise errors:
// (a) "99.99**99999999" ==>
//   OverflowError: (34, 'Result too large')
// (b) "9999**9.9" ==>
//   TypingError: Invalid usage of getitem with parameters (float64, int64)
//    * parameterized
//   File "<string>", line 3
//   Failed at nopython (nopython frontend)
//   Invalid usage of getitem with parameters (float64, int64)
//    * parameterized
//   File "<string>", line 3
//      Note that 9999**9.9 equals about 3.977E39, which is a legitimate
//      float64 value.
// (c) "99999999999999999999" ==>
//   ValueError: Int value is too large: 99999999999999999999
// (d) "9.9e99" ==>
//   SAME ERROR MSG AS GENERATED BY (b)
// (e) "pow(9999,99999999)" ==>
//   NameError: name 'pow' is not defined

//(4) "''.join(str(x/(y+1)) for x in xrange(99999) for y in xrange(99999))" ==>
//   NameError: name 'xrange' is not defined
//And "''.join(str(x/(y+1)) for x in range(99999) for y in range(99999))" ==>
//   NameError: name 'range' is not defined

}

@martinholmer
Copy link
Collaborator Author

martinholmer commented Dec 3, 2016

In the course of trying to stress Tax-Calculator using the parameter-code capability in pull request #1081, I discovered that the current version of TaxBrain is not immune to abuse. Obviously, you can't enter any numerical values for policy parameters that allow access to the underlying operating system and I have't found any numerical values that cause TaxBrain to crash, but I have found that you can enter numerical values that produce absurd results.

So, for example, a common way of characterizing a reform that subjects all earnings to the OASDI payroll tax is to enter for the maximum taxable earnings parameter a value of 9.9e99, which generates a 2020 increase in payroll tax revenue of 183.1 billion dollars (complete results on this page). But if I enter for this same policy parameter a value of 9.9e99**9.9e99, TaxBrain generates a 2020 increase in payroll tax revenue of -956.7 billion dollars (complete results on this page). That is a BIG difference with the drop in revenue being clearly absurd.

So, as we consider the benefits and costs of merging pull request #1081, let's not forget that the current version of TaxBrain is already open to minor abuse by devious users.

@MattHJensen @feenberg @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher @codykallen

@feenberg
Copy link
Contributor

feenberg commented Dec 3, 2016 via email

@codykallen
Copy link
Contributor

The decrease in payroll taxes resulting from setting the maximum taxable earnings for Social Security to 9.9e99**9.9e99 is due to Python's method of handling NaN entries in data. This value results in an error for the calculation of payroll taxes in the reform. When the change in revenue is calculated, Python treats the NaN value as zero by default. The payroll tax decrease of $956.7 billion in 2020 is simply the total revenue from Social Security taxes under current law. The same revenue change can be achieved by setting the maximum taxable earnings to zero.

@feenberg
Copy link
Contributor

feenberg commented Dec 3, 2016 via email

@martinholmer
Copy link
Collaborator Author

@codykallen said:

The decrease in payroll taxes resulting from setting the maximum taxable earnings for Social Security to 9.9e99**9.9e99 is due to Python's method of handling NaN entries in data. This value results in an error for the calculation of payroll taxes in the reform. When the change in revenue is calculated, Python treats the NaN value as zero by default. The payroll tax decrease of $956.7 billion in 2020 is simply the total revenue from Social Security taxes under current law. The same revenue change can be achieved by setting the maximum taxable earnings to zero.

Thanks for this informative comment.

Then @feenberg said:

I just went to TB and entered 0.5*min(100000,e00100) into the box for the taxable maximum wages. The result seems plausible, so I have to think that TB accepts expressions in the boxes that are documented to require numeric literals.

This is a fantastic feature, which will allow many wonderful reforms to be tested directly but it does mean that we have to vet the inputs for possibly abusive content. I believe Martin has the code to do that.

The practice of converting NaNs to zero seems unjustified to me. My local Python interpreter does not do that - can we turn it off and what would we lose if we did so? I would prefer TB to return a server error, preferably with some diagnostic information.

If expressions are being evaluated when entered into TaxBrain numerical parameter boxes, then it would seem as if the current version of TaxBrain (without pull request #1081) may have some security risks.

@MattHJensen @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher

@martinholmer
Copy link
Collaborator Author

@feenberg said:

I just went to TB and entered 0.5*min(100000,e00100) into the box for the taxable maximum wages [that is, the OASDI maximum taxable earnings]. The result seems plausible, so I have to think that TB accepts expressions in the boxes that are documented to require numeric literals.

When I did what you describe above on TaxBrain it did run to completion and showed me results. When you say that the results "seems plausible" I wasn't sure what your expectations were. The results show a decline in 2020 payroll tax revenue of $956.7 billion from a pre-reform amount of $1,045.3 billion. This seems the same as @codykallen described above with the maximum taxable earnings parameter being interpreted as zero. This could be for one of (at least) two reasons. First, the expression may have been evaluated by TaxBrain as zero because when the _SS_Earnings_c parameter is used in the EI_PayrollTax function that e00100 is not visible (and perhaps, therefore, interpreted as zero). Or second, it could be that the expression that you entered is simply not comprehended as a numerical value, but TaxBrain goes ahead and does all the computations anyway.

In an attempt to see which of these two things is going on in TaxBrain, I tried an expression that includes a variable that is visible in the EI_PayrollTax function. When I enter 1.0*min(118500, e00200) for the value of the maximum taxable earnings, I expect to see little or no change in payroll tax revenue. The TaxBrain results that I get look just like what you got with your expression with 2020 payroll tax revenue declining by about 91 percent (see the complete results here). This suggest to me that there is no evaluation of the expression by TaxBrain, it is just that TaxBrain is totally confused and assigns the _SS_Earnings_c parameter a value of zero or (as @codykallen suggests) NaN.

So, rather than viewing your results as an indication that the current version of TaxBrain has hidden in it "a fantastic feature", I view these results is indicating that the current version of TaxBrain has a serious bug. My suspicion is that this behavior is rooted in the philosophy of the project to try to avoid any validation testing of user-supplied input, but that is just a guess based on no systematic investigation.

By the way, if you enter these kinds of expressions for the maximum taxable earnings parameter using the JSON reform file approach to using TaxBrain, errors messages are generated and no tax calculations are done.

@MattHJensen @feenberg @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher @codykallen

@feenberg
Copy link
Contributor

feenberg commented Dec 4, 2016 via email

@talumbau
Copy link
Member

talumbau commented Dec 5, 2016

I have been out-of-pocket for some time, but I saw some of the messages here so I decided to get caught up on this thread. Overall, I have two main pieces of feedback:

  1. The only way to be able to make any kind of safety guarantee on running user-submitted Python code on TaxBrain is to provide a full sandbox environment as @zrisher points out (details below). It is not good practice to read in user-submitted code and then attempt to perform various checks on it prior to execution via eval.

  2. This thread reveals a bug in TaxBrain regarding the processing of inputs.

For item 1, the salient points from @zrisher are as follows:

I'm pretty sure there are separate servers for webapp-public vs the taxcalc workers they queue up, but if an anonymous user has the ability to run unsandboxed code without review, they can bring down the entire tax calculation side by simply locking up all the workers with multiple long-running requests. Even with auto-scaling, it's an easy way to deny that service. I'm sure the worker controller has some safeguards built in here that @talumbau could speak to, but I'd bet that no matter how it's architected, a malicious user could easily deny the tax calculation service in that situation.

I don't think it's safe for direct use by web users by any means (not sure if that's what you're asking), but it's totally safe for local use.

A user could control things we don't want them to - This is inherently difficult to solve because Python provides such powerful metaprogramming abilities regardless of your execution level. This is exemplified by the links @feenberg provided, explained by the top answer to this SO post, and proven out by the death of the language-level sandbox pysandbox. PyPy attempts to solve this by replacing even the lowest level references with its safer constructs.

The issues with eval are covered nicely in the links in this thread, so there's no need to go in to additional detail. A good question is raised though: what are the consequences of malicious code execution on TaxBrain? I see two:

  • reducing quality of service by locking up worker nodes or killing the processes that run the TaxBrain jobs

  • gaining root access to the AWS machines and using them for nefarious purposes

The first is more likely than the second, which is mostly just annoying and would create a higher maintenance burden on TaxBrain. The second is obviously very bad, but does not seem very likely. Still the consequences are bad enough that we should try very hard to avoid it.

For sandboxing, PyPy is not a viable alternative due to the fact that Tax-Calculator uses numba for better performance when executing the functions in functions.py, and it has numerous dependencies on numpy and related projects (e.g. pandas).

The suggestion I put forward about this issue (executing user-submitted code on TaxBrain) at our meeting in April is to use Docker as the container mechanism. The Dockerized system would be configured in a way similar to what is described in the StackExchage post linked above. The idea is that any malicious code would only be able to be malicious on an isolated system that could not impact the actual worker node. The effort level here is not small.

For item 2, it's clear that the TaxBrain input page needs additional logic for better custom input parsing. This is a consequence of the decision to make these input fields a custom field, which means we are responsible for parsing what the user types. For example, initially we allowed just this kind of input:

7500, 7750, 8000

but then we modified this to include things like:

*, *, 9000

If we only allowed integer literals or float literals or something like that, we could just hand the input off to the int function or the float function, respectively, but since we have gone down the road of a custom input, we have to roll up our sleeves and make sure we are doing everything correctly (including raising appropriate errors). It seems clear we are currently not.

I'm back from my break on Tuesday, so I will be able to engage on the TaxBrain input errors at that time.

@feenberg
Copy link
Contributor

feenberg commented Dec 5, 2016 via email

@martinholmer
Copy link
Collaborator Author

A more comprehensive report on TaxBrain problems when entering an expression (rather than a number) into a traditional parameter box is provided in TaxBrain issue 427.

@MattHJensen @feenberg @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher @talumbau

@martinholmer
Copy link
Collaborator Author

I would like to merge pull request #1081 at the end of the work day on Wednesday, December 7th.

We seem to have a consensus (in the discussion so far) that the parameter-code enhancements in #1081 are OK for use with Tax-Calculator on local computers. There does seem to be some difference of opinion about the consequences of allowing the parameter-code enhancements to run on TaxBrain. But that is no problem for #1081 because it would be very easy for TaxBrain to refuse to execute any uploaded JSON reform file that contains the phrase param_code. This means we can get the benefits of #1081 on our local computers and decide exactly when (or if) to allow parameter-code in JSON reform files uploaded to TaxBrain.

So, if you have any concerns about merging #1081 into the master branch, now is the time to share those concerns with the development team.

@MattHJensen @feenberg @Amy-Xu @GoFroggyRun @andersonfrailey @zrisher @talumbau @codykallen

@martinholmer
Copy link
Collaborator Author

martinholmer commented Dec 6, 2016

@talumbau, Does commit 984f1ca in pull request #1081 provide what you requested for TaxBrain?

@zrisher
Copy link
Contributor

zrisher commented Dec 7, 2016

I'm 👍 on merging this for use by local taxcalc users.

The discussion on TB incorrectly parsing param input has been moved to ospc-org/ospc.org#427.

I suggest we continue the discussion of improving the ability of users to programmatically customize tax calculation logic in #429. This PR is our first step in that direction.

@martinholmer
Copy link
Collaborator Author

After merging recent master branch changes into the pull-request-1081 development branch, we have the following results showing that the new param_code feature produces exactly the same results as the master-branch hardwired code being replaced.

$ cp ../tax-calculator-data/puf.csv .

$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

$ cat ref-old.json
{
"policy": {
    "_ALD_Investment_ec_base_all": {"2016": [false]},
    "_ALD_Investment_ec_rt": {"2016": [0.50]}
},
"behavior": {}, "growth": {}, "consumption": {}
}

$ python inctax.py puf.csv 2016 --reform ref-old.json

$ ls -l puf-16.out-inctax-ref-old
-rw-r--r--  1 mrh  staff  37776134 Dec  7 07:54 puf-16.out-inctax-ref-old

$ git checkout invinc-ec-base-code
Switched to branch 'invinc-ec-base-code'

$ cat ref-new.json 
{
"policy": {
    "_ALD_Investment_ec_base_code_active": {"2016": [true]},
    "param_code": {"ALD_Investment_ec_base_code": "e00300 + e00650 + p23250"},
    "_ALD_Investment_ec_rt": {"2016": [0.50]}
},
"behavior": {}, "growth": {}, "consumption": {}
}

$ python inctax.py puf.csv 2016 --reform ref-new.json

$ ls -l puf-16.out-inctax-ref-new
-rw-r--r--  1 mrh  staff  37776134 Dec  7 07:56 puf-16.out-inctax-ref-new

$ diff puf-16.out-inctax-ref-new puf-16.out-inctax-ref-old

$ 

@talumbau
Copy link
Member

talumbau commented Dec 7, 2016

+1 from me. Thanks for adding the switch @martinholmer

@martinholmer
Copy link
Collaborator Author

@talumbau said with respect to the Policy.PROHIBIT_PARAM_CODE switch:

+1 from me. Thanks for adding the switch

OK. Unless I heard any further concerns, I'll merge pull request #1081 at the end of Wed, 12/7.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants